Initial commit: trueref v0.1.0-SNAPSHOT
Some checks failed
Build and publish Docker image / Build and push (push) Failing after 1m27s
Some checks failed
Build and publish Docker image / Build and push (push) Failing after 1m27s
Java 21 / Spring Boot 3.5.3 multi-module Maven project. Hybrid BM25+HNSW search with RRF, cross-encoder reranker, ONNX Runtime 1.22.0 (CPU + CUDA 12 GPU variants).
This commit is contained in:
68
trueref-bootstrap/pom.xml
Normal file
68
trueref-bootstrap/pom.xml
Normal file
@@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.trueref</groupId>
|
||||
<artifactId>trueref-parent</artifactId>
|
||||
<version>0.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>trueref-bootstrap</artifactId>
|
||||
<name>trueref-bootstrap</name>
|
||||
<description>Spring Boot entry point. Wires beans across modules. Produces the executable fat JAR.</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.trueref</groupId>
|
||||
<artifactId>trueref-domain</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.trueref</groupId>
|
||||
<artifactId>trueref-application</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.trueref</groupId>
|
||||
<artifactId>trueref-adapters</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.trueref</groupId>
|
||||
<artifactId>trueref-frontend</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.micrometer</groupId>
|
||||
<artifactId>micrometer-registry-prometheus</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<finalName>trueref</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<mainClass>com.trueref.bootstrap.TrueRefApplication</mainClass>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals><goal>repackage</goal></goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.trueref.bootstrap;
|
||||
|
||||
import com.trueref.application.catalog.CatalogService;
|
||||
import com.trueref.application.ingest.DiscoveryService;
|
||||
import com.trueref.application.ingest.IngestionOrchestrator;
|
||||
import com.trueref.application.observability.InMemoryJobEventBus;
|
||||
import com.trueref.application.observability.JobObservationService;
|
||||
import com.trueref.application.resolve.LibraryResolver;
|
||||
import com.trueref.application.search.HybridSearchService;
|
||||
import com.trueref.domain.port.out.ChunkStore;
|
||||
import com.trueref.domain.port.out.CodeParser;
|
||||
import com.trueref.domain.port.out.EmbeddingCache;
|
||||
import com.trueref.domain.port.out.EmbeddingService;
|
||||
import com.trueref.domain.port.out.GitClient;
|
||||
import com.trueref.domain.port.out.JobStore;
|
||||
import com.trueref.domain.port.out.RepositoryStore;
|
||||
import com.trueref.domain.port.out.RerankerService;
|
||||
import java.nio.file.Path;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Explicit bean wiring for the application layer (which stays Spring-annotation-free).
|
||||
* We expose only concrete beans; Spring resolves interface dependencies against the single
|
||||
* concrete implementation.
|
||||
*/
|
||||
@Configuration
|
||||
public class ApplicationBeans {
|
||||
|
||||
@Bean
|
||||
Path trueRefHome(@Value("${trueref.home:./data}") String home) {
|
||||
return Path.of(home);
|
||||
}
|
||||
|
||||
@Bean
|
||||
InMemoryJobEventBus jobEventBus() {
|
||||
return new InMemoryJobEventBus();
|
||||
}
|
||||
|
||||
@Bean
|
||||
CatalogService catalogService(RepositoryStore store, Path trueRefHome) {
|
||||
return new CatalogService(store, trueRefHome);
|
||||
}
|
||||
|
||||
@Bean
|
||||
DiscoveryService discoveryService(RepositoryStore store, GitClient git) {
|
||||
return new DiscoveryService(store, git);
|
||||
}
|
||||
|
||||
@Bean(destroyMethod = "shutdown")
|
||||
IngestionOrchestrator ingestionOrchestrator(
|
||||
RepositoryStore repoStore,
|
||||
JobStore jobStore,
|
||||
ChunkStore chunkStore,
|
||||
EmbeddingService embeddings,
|
||||
EmbeddingCache embeddingCache,
|
||||
GitClient git,
|
||||
CodeParser parser,
|
||||
InMemoryJobEventBus bus,
|
||||
@Value("${trueref.ingestion.max-parse-jobs:4}") int maxParseJobs,
|
||||
@Value("${trueref.ingestion.embed-queue-capacity:4}") int embedQueueCapacity) {
|
||||
return new IngestionOrchestrator(
|
||||
repoStore, jobStore, chunkStore, embeddings, embeddingCache, git, parser, bus,
|
||||
maxParseJobs, embedQueueCapacity);
|
||||
}
|
||||
|
||||
@Bean
|
||||
LibraryResolver libraryResolver(RepositoryStore store, IngestionOrchestrator indexer) {
|
||||
return new LibraryResolver(store, indexer);
|
||||
}
|
||||
|
||||
@Bean
|
||||
HybridSearchService hybridSearchService(
|
||||
ChunkStore chunks,
|
||||
EmbeddingService embedder,
|
||||
RerankerService reranker,
|
||||
RepositoryStore repos,
|
||||
@Value("${trueref.search.rrf-k:60}") int rrfK,
|
||||
@Value("${trueref.reranker.top-k:50}") int rerankTopK,
|
||||
@Value("${trueref.search.final-top-k:20}") int finalTopK) {
|
||||
return new HybridSearchService(chunks, embedder, reranker, repos, rrfK, rerankTopK, finalTopK);
|
||||
}
|
||||
|
||||
@Bean
|
||||
JobObservationService jobObservationService(JobStore jobs, InMemoryJobEventBus bus) {
|
||||
return new JobObservationService(jobs, bus);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.trueref.bootstrap;
|
||||
|
||||
import com.trueref.application.ingest.DiscoveryService;
|
||||
import com.trueref.domain.model.Repository;
|
||||
import com.trueref.domain.model.Version;
|
||||
import com.trueref.domain.model.VersionStatus;
|
||||
import com.trueref.domain.port.in.IndexVersion;
|
||||
import com.trueref.domain.port.out.RepositoryStore;
|
||||
import java.time.Instant;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/** Periodically fetches tags for registered repos and enqueues indexing for new ones. */
|
||||
@Component
|
||||
@EnableScheduling
|
||||
@ConditionalOnProperty(name = "trueref.ingestion.poller-enabled", havingValue = "true", matchIfMissing = true)
|
||||
public class ScheduledPoller {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ScheduledPoller.class);
|
||||
|
||||
private final RepositoryStore repoStore;
|
||||
private final DiscoveryService discovery;
|
||||
private final IndexVersion indexer;
|
||||
|
||||
public ScheduledPoller(RepositoryStore repoStore, DiscoveryService discovery, IndexVersion indexer) {
|
||||
this.repoStore = repoStore;
|
||||
this.discovery = discovery;
|
||||
this.indexer = indexer;
|
||||
}
|
||||
|
||||
@Scheduled(fixedDelayString = "${trueref.ingestion.poll-interval-default:PT1H}")
|
||||
public void pollAll() {
|
||||
Instant start = Instant.now();
|
||||
int scanned = 0;
|
||||
int enqueued = 0;
|
||||
for (Repository repo : repoStore.findAll()) {
|
||||
try {
|
||||
discovery.discover(repo.id());
|
||||
for (Version v : repoStore.findVersionsByRepo(repo.id())) {
|
||||
if (v.status() == VersionStatus.DISCOVERED) {
|
||||
indexer.enqueue(repo.id(), v.id(), false);
|
||||
enqueued++;
|
||||
}
|
||||
}
|
||||
scanned++;
|
||||
} catch (Exception e) {
|
||||
log.warn("poll failed for repo={}: {}", repo.name(), e.toString());
|
||||
}
|
||||
}
|
||||
log.info("poll completed in {}ms: repos scanned={} jobs enqueued={}",
|
||||
java.time.Duration.between(start, Instant.now()).toMillis(), scanned, enqueued);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.trueref.bootstrap;
|
||||
|
||||
import com.trueref.domain.port.out.JobStore;
|
||||
import com.trueref.domain.port.out.RepositoryStore;
|
||||
import java.time.Instant;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* On-startup cleanup for stale job state left by a previous crash or SIGKILL.
|
||||
*
|
||||
* <p>Any job that is RUNNING or QUEUED when the application starts must have been orphaned by a
|
||||
* previous JVM exit (clean or unclean). We fail them all atomically before accepting traffic so
|
||||
* the UI and API never show phantom RUNNING jobs. Matching INDEXING versions are also reset to
|
||||
* FAILED so they can be re-queued immediately.
|
||||
*
|
||||
* <p>This fires <em>after</em> Flyway migrations and all beans are initialised, but before the
|
||||
* application starts accepting HTTP requests (ApplicationReadyEvent fires before the embedded
|
||||
* Tomcat connector starts accepting connections).
|
||||
*/
|
||||
@Component
|
||||
class StaleJobCleanupStartup {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(StaleJobCleanupStartup.class);
|
||||
|
||||
private static final String RESTART_REASON = "interrupted by server restart";
|
||||
|
||||
private final JobStore jobStore;
|
||||
private final RepositoryStore repositoryStore;
|
||||
|
||||
StaleJobCleanupStartup(JobStore jobStore, RepositoryStore repositoryStore) {
|
||||
this.jobStore = jobStore;
|
||||
this.repositoryStore = repositoryStore;
|
||||
}
|
||||
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void cleanupStaleJobs() {
|
||||
Instant now = Instant.now();
|
||||
|
||||
int failedJobs = jobStore.failStaleJobs(now);
|
||||
if (failedJobs > 0) {
|
||||
log.warn(
|
||||
"Startup cleanup: marked {} orphaned job(s) as FAILED (were RUNNING or QUEUED at shutdown).",
|
||||
failedJobs);
|
||||
} else {
|
||||
log.info("Startup cleanup: no stale jobs found.");
|
||||
}
|
||||
|
||||
int failedVersions = repositoryStore.failStaleIndexingVersions(RESTART_REASON);
|
||||
if (failedVersions > 0) {
|
||||
log.warn(
|
||||
"Startup cleanup: reset {} INDEXING version(s) to FAILED (their jobs did not complete).",
|
||||
failedVersions);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.trueref.bootstrap;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* Trueref entry point. The only place where Spring component scanning is allowed across the
|
||||
* {@code com.trueref} package tree. Adapters and application modules expose explicit
|
||||
* {@code @Configuration} classes that this class imports via component scanning.
|
||||
*/
|
||||
@SpringBootApplication(scanBasePackages = "com.trueref")
|
||||
public class TrueRefApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(TrueRefApplication.class, args);
|
||||
}
|
||||
}
|
||||
114
trueref-bootstrap/src/main/resources/application.yml
Normal file
114
trueref-bootstrap/src/main/resources/application.yml
Normal file
@@ -0,0 +1,114 @@
|
||||
spring:
|
||||
application:
|
||||
name: trueref
|
||||
threads:
|
||||
virtual:
|
||||
enabled: true
|
||||
datasource:
|
||||
url: jdbc:h2:file:${trueref.home:./data}/h2/trueref;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MV_STORE=TRUE
|
||||
username: sa
|
||||
password: ""
|
||||
driver-class-name: org.h2.Driver
|
||||
hikari:
|
||||
# Embedded H2 serialises writes internally; 8 connections is ample for virtual-thread
|
||||
# workloads. 32 is wasteful and causes unnecessary H2 lock contention.
|
||||
maximum-pool-size: 8
|
||||
minimum-idle: 2
|
||||
flyway:
|
||||
enabled: true
|
||||
locations: classpath:db/migration
|
||||
mvc:
|
||||
async:
|
||||
request-timeout: 0 # SSE streams must not time out
|
||||
# Spring AI MCP server. In Spring AI 1.0.0 the WebMVC transport is SSE-based
|
||||
# (WebMvcSseServerTransportProvider) — the closest available transport to the 2025-03-26
|
||||
# "Streamable HTTP" spec; there is no separate "protocol: streamable" property in this
|
||||
# starter. JSON-RPC POSTs land on `sse-message-endpoint` (/mcp); server-initiated
|
||||
# notifications stream over `sse-endpoint` (/sse). See com.trueref.adapter.in.mcp.
|
||||
ai:
|
||||
mcp:
|
||||
server:
|
||||
enabled: true
|
||||
name: trueref
|
||||
version: 0.1.0
|
||||
type: SYNC
|
||||
sse-message-endpoint: /mcp
|
||||
sse-endpoint: /sse
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
shutdown: graceful
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics,prometheus
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
|
||||
springdoc:
|
||||
api-docs:
|
||||
path: /v3/api-docs
|
||||
swagger-ui:
|
||||
path: /swagger-ui.html
|
||||
|
||||
trueref:
|
||||
home: ${TRUEREF_HOME:./data}
|
||||
ingestion:
|
||||
poll-interval-default: PT1H
|
||||
tag-cap-default: 100
|
||||
max-file-size-bytes-default: 1048576
|
||||
watched-folder: ${trueref.home}/watched
|
||||
# Max parallel parse jobs (FETCH/CLONE → CHECKOUT → DISCOVER → DIFF → PARSE).
|
||||
# Parse is I/O + CPU only — no GPU. 4 is safe on this machine (Ryzen 9 3900X, 62 GB RAM);
|
||||
# increase for repos with small files, decrease if git I/O saturates disk.
|
||||
max-parse-jobs: 4
|
||||
# Max parsed batches buffered between parse workers and the embed worker.
|
||||
# When the embed worker is busy, parse workers block here — natural backpressure.
|
||||
# Total peak in-memory batches = max-parse-jobs + embed-queue-capacity.
|
||||
embed-queue-capacity: 4
|
||||
embedding:
|
||||
model: bge-base-en-v1.5
|
||||
onnx-providers: cuda,directml,cpu
|
||||
session-count: 1
|
||||
batch-size: 32
|
||||
max-seq-len: 512
|
||||
# Which CUDA device to bind ONNX sessions to. Passed directly to ORT's CUDA EP
|
||||
# as the physical device index — ORT uses the CUDA driver/NVML API which can bypass
|
||||
# CUDA_VISIBLE_DEVICES remapping. The ./trueref script sets this to $TRUEREF_GPU (default: 1 = RTX 3060).
|
||||
gpu-device-id: 0
|
||||
# Per-session GPU memory cap in bytes. 0 = unbounded. With session-count=1 there
|
||||
# is no pool contention, so leave this unbounded — capping it risks exhausting the
|
||||
# BFC arena during model-weight loading before inference starts. The ./trueref script
|
||||
# defaults to 0 and can be overridden with TRUEREF_MEM_LIMIT.
|
||||
gpu-mem-limit-bytes: 0
|
||||
# Override download URLs per (model, file). The built-in defaults (in ModelDownloader)
|
||||
# cover bge-base-en-v1.5, ms-marco-MiniLM-L6-v2, bge-m3, and bge-reranker-v2-m3.
|
||||
# Set HF_TOKEN in the environment for higher rate limits or gated models.
|
||||
# model-sources:
|
||||
# bge-base-en-v1.5:
|
||||
# model.onnx:
|
||||
# - https://huggingface.co/BAAI/bge-base-en-v1.5/resolve/main/onnx/model.onnx
|
||||
reranker:
|
||||
model: ms-marco-MiniLM-L6-v2
|
||||
top-k: 100
|
||||
embedding-cache:
|
||||
# Must match the embedding model's output dimension. Changing this automatically
|
||||
# wipes the stale .f32 files in the cache directory on next startup.
|
||||
dimension: 768
|
||||
search:
|
||||
rrf-k: 60
|
||||
final-top-k: 20
|
||||
mcp:
|
||||
tokens-default: 5000
|
||||
tokens-min: 500
|
||||
tokens-max: 50000
|
||||
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
com.trueref: INFO
|
||||
org.eclipse.jgit: WARN
|
||||
org.apache.lucene: WARN
|
||||
62
trueref-bootstrap/src/main/scripts/trueref
Executable file
62
trueref-bootstrap/src/main/scripts/trueref
Executable file
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env bash
|
||||
# trueref launcher — wraps the fat JAR with the JVM flags required to silence
|
||||
# the FFM (foreign linker) restricted-method warning emitted by JNA-based
|
||||
# tokenizer libraries and to make Lucene's Vector API path readable.
|
||||
#
|
||||
# --enable-native-access=ALL-UNNAMED
|
||||
# Lucene 10 + DJL HuggingFace Tokenizers use the new java.lang.foreign
|
||||
# Linker API; on Java 21 this requires explicit native-access opt-in.
|
||||
# --add-modules jdk.incubator.vector
|
||||
# Lucene 10 ships an incubator-vector codepath that is significantly
|
||||
# faster for cosine/dot-product math but only loads if the module is
|
||||
# made readable from the unnamed module.
|
||||
#
|
||||
# Usage:
|
||||
# bin/trueref # default settings
|
||||
# bin/trueref --server.port=18080 # forward Spring properties
|
||||
# TRUEREF_JAR=/path/to/trueref.jar bin/trueref
|
||||
#
|
||||
# Environment overrides:
|
||||
# TRUEREF_JAR Path to the fat JAR (default: <script-dir>/../trueref.jar)
|
||||
# JAVA Path to the java binary (default: ${JAVA_HOME:-}/bin/java or `java` on PATH)
|
||||
# JAVA_OPTS Extra JVM flags (e.g. -Xmx16g, -XX:+UseZGC)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
JAR_DEFAULT="${SCRIPT_DIR}/../trueref.jar"
|
||||
JAR="${TRUEREF_JAR:-$JAR_DEFAULT}"
|
||||
|
||||
if [[ ! -f "$JAR" ]]; then
|
||||
echo "trueref: jar not found at $JAR" >&2
|
||||
echo "trueref: set TRUEREF_JAR or place trueref.jar next to this script" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "${JAVA:-}" ]]; then
|
||||
:
|
||||
elif [[ -n "${JAVA_HOME:-}" && -x "${JAVA_HOME}/bin/java" ]]; then
|
||||
JAVA="${JAVA_HOME}/bin/java"
|
||||
else
|
||||
JAVA="$(command -v java || true)"
|
||||
fi
|
||||
|
||||
if [[ -z "${JAVA:-}" || ! -x "${JAVA}" ]]; then
|
||||
echo "trueref: java not found; set JAVA_HOME or install JDK 21+" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ONNX Runtime CUDA EP needs cuDNN 9 on LD_LIBRARY_PATH. Many distros only ship
|
||||
# cuDNN via the system package manager or via a Python wheel (nvidia-cudnn-cu12).
|
||||
# If the user sets TRUEREF_CUDNN_LIB we trust it; otherwise we leave LD_LIBRARY_PATH
|
||||
# alone and let CUDA fall back to CPU with a logged warning.
|
||||
if [[ -n "${TRUEREF_CUDNN_LIB:-}" ]]; then
|
||||
export LD_LIBRARY_PATH="${TRUEREF_CUDNN_LIB}${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
||||
fi
|
||||
|
||||
exec "$JAVA" \
|
||||
--enable-native-access=ALL-UNNAMED \
|
||||
--add-modules=jdk.incubator.vector \
|
||||
${JAVA_OPTS:-} \
|
||||
-jar "$JAR" \
|
||||
"$@"
|
||||
Reference in New Issue
Block a user