Skip to content

Modernize Gradle, add hybrid vector search and GitHub ingestion - #10

Merged
WilliamAGH merged 164 commits into
mainfrom
dev
Feb 8, 2026
Merged

Modernize Gradle, add hybrid vector search and GitHub ingestion#10
WilliamAGH merged 164 commits into
mainfrom
dev

Conversation

@WilliamAGH

Copy link
Copy Markdown
Owner

This pull request introduces several structural and standards improvements across the project, focusing on build/configuration hygiene, development workflow, and coding standards documentation. The changes clean up obsolete files, centralize configuration, enhance Makefile maintainability, and significantly expand the coding standards in AGENTS.md.

Key changes:

Build and Configuration Hygiene:

  • Removed .dockerignore, .gitattributes, and .pre-commit-config.yaml as part of streamlining and centralizing configuration management. This reduces redundancy and potential confusion by consolidating build and formatting logic elsewhere. [1][2][3]
  • Updated the Dockerfile to move static analysis configs (pmd, spotbugs) into the config/ directory, and added JVM flags for native access and memory safety. Also removed an unused environment variable. [1][2][3]

Development Workflow Improvements:

  • Refactored the Makefile to source shared logic and variables from config/make/common.mk, greatly reducing duplication and centralizing environment handling, argument building, and port management. New targets were added for more granular doc/repo processing. [1][2][3][4]
  • Updated health checks and utility scripts to use the new default port variable, improving consistency and maintainability.

Documentation and Coding Standards:

  • Expanded AGENTS.md with new and clarified rules, especially around naming discipline (banning generic/abbreviated names, enforcing intent-revealing identifiers), Git safety, and explicit root cause handling. Added new rule sections and extended existing ones for stricter agent guidance. [1][2][3][4][5]
  • Updated README.md with a new banner image and improved project description for clarity and marketing.

Summary of most important changes:

Build/configuration cleanup:

  • Removed obsolete or redundant files: .dockerignore, .gitattributes, .pre-commit-config.yaml. [1][2][3]
  • Moved static analysis configs to config/ and improved Docker JVM flags. [1][2]

Makefile and workflow enhancements:

  • Refactored Makefile to use shared includes, centralized environment/argument logic, and added new doc/repo processing targets. [1][2][3][4]

Coding standards and documentation:

  • Major expansion and clarification of rules in AGENTS.md, especially for naming, Git behavior, and error handling. [1][2][3][4][5]
  • Improved README.md with a new banner and clearer project summary.

CSRF tokens previously lived for the entire session, which conflicts with the
short‑lived token requirement. This enforces a 15‑minute TTL and returns clear
JSON 403 responses so clients can refresh intentionally.
- Track token + issued-at in the session and invalidate after TTL
- Return JSON 403 with expired vs missing/invalid messaging
- Wire custom repository + access denied handler into security config
CSRF expirations were surfacing as opaque 403s with no recovery path, and API
error payloads were not parsed for user-facing context. This adds a CSRF-aware
retry flow with validated error parsing and a toast UI so users get actionable
feedback when their session expires.
- Add API error schema to validate error payloads
- Implement CSRF refresh + retry flow with throttled toast notification
- Wire chat and SSE requests to use the retry and error extraction helpers
- Add toast store + container to surface session-expired messages
Provider selection previously fell back to another provider even when the active
provider was rate-limited, risking silent provider switches. This change stops
fallback on rate limits, returns null when the active provider is unavailable,
and clarifies the error message surfaced to callers.
- Stop auto-fallback when OpenAI or GitHub Models is rate-limited
- Emit explicit log warnings when fallback is disabled
- Update unavailable-provider error message to reflect rate limit/misconfig
Local executable artifacts in bin/ should not be tracked. This adds the
folder to .gitignore to prevent accidental commits.
The project root was cluttered with 12+ config files for various tools
(checkstyle, pmd, spotbugs, ast-grep, prek, htmlhint). This makes it
harder to navigate the project and identify core source files. Moving
these to a dedicated config/ directory follows the Gradle convention
for tool configs and creates a cleaner project structure.
- Move checkstyle.xml to config/checkstyle/
- Move pmd-ruleset.xml to config/pmd/
- Move spotbugs-*.xml to config/spotbugs/
- Move rules/ast-grep/* to config/ast-grep/
- Move sgconfig.yml and prek.toml to config/
- Move .hintrc and .htmlhintrc to frontend/ (where they belong)
- Rename LICENSE to LICENSE.md for consistency
…udes
Dependency versions were scattered across build.gradle.kts as 14+ local
variables, violating DRY and making upgrades error-prone. The Makefile
had ~90 lines of duplicated shell logic across run/dev/dev-backend targets
for env loading, API key validation, and argument building. This refactor
centralizes configuration and reduces duplication.
Gradle changes:
- Create gradle/libs.versions.toml with centralized version catalog
- Define library bundles (spring-boot-web, spring-ai, flexmark-all, testing)
- Use type-safe accessors like libs.bundles.spring.boot.web
- Plugin versions managed via catalog aliases
Makefile changes:
- Extract common variables to config/make/common.mk
- Define reusable functions: load_env, validate_api_keys, get_server_port,
free_port, build_app_args
- Centralize DEFAULT_JAVA_OPTS and GRADLE_JVM_ARGS constants
- Reduce run/dev/dev-backend targets from ~90 duplicated lines to function calls
Add a visual preview of the application at the top of README following
GitHub conventions for project documentation. The screenshot is placed
in the idiomatic Java resources location and links to the live demo.
- Move java-chat-app.png to src/main/resources/static/images/
- Add clickable screenshot banner linking to https://javachat.ai
- Update LICENSE reference to LICENSE.md
Move docker-compose-qdrant.yml to infra/ directory for better organization
of infrastructure files. Remove .gitattributes which contained Maven wrapper
line-ending rules (/mvnw, *.cmd) that are obsolete since the project migrated
to Gradle.
- Move docker-compose-qdrant.yml → infra/docker-compose-qdrant.yml
- Delete .gitattributes (Maven wrapper rules, no longer applicable)
The pre-commit Python tool configuration is unused - git hooks are
implemented as bash scripts in .git/hooks/ that run make targets
directly. The prek.toml in config/ documents the hook behavior.
- Remove .pre-commit-config.yaml (hooks use bash scripts instead)
Remove standalone .dockerignore since Docker now respects .gitignore
patterns for build context. Reorganize .gitignore with clear section
headers. Update Dockerfile to reference tool configs in their new
config/ directory locations.
- Delete .dockerignore (patterns consolidated into .gitignore)
- Reorganize .gitignore with section headers and remove duplicates
- Update Dockerfile COPY paths for pmd/spotbugs configs in config/
The previous COPY command flattened config files to /app/ root, but
build.gradle.kts expects them at config/pmd/ and config/spotbugs/ paths.
- Copy config/pmd/ and config/spotbugs/ directories instead of individual files
API response records lived in the web package, tying core response
models to the delivery layer and making reuse across adapters harder.
Move the response contract into the domain errors package and update
controllers/builders to return the domain types.
- Relocate ApiResponse/ApiErrorResponse/ApiSuccessResponse into domain/errors
- Add invariant checks to the domain response records
- Update controllers and builders to return domain ApiResponse
- Remove the web-layer response records
Ingestion endpoints mixed generic ApiResponse contracts with service
failure types, which blurred the public API shape and leaked service
details. Define explicit ingestion response contracts in the domain
and map local failures into domain value types.
- Add ingestion response interfaces and outcome records in domain/ingestion
- Introduce IngestionLocalFailure to avoid leaking service-layer types
- Return IngestionRunOutcome/IngestionLocalOutcome on success
- Return IngestionErrorResponse for ingestion errors and validation failures
- Remove the web-layer IngestionLocalResponse
OpenAI streams sometimes emit markdown headings (especially `#` or setext-style),
which map to `h1`. In the chat bubble styles, `h1` and `h4` were not explicitly
sized, so Safari/iOS fell back to large default heading sizes and produced
unexpected typography jumps mid-stream. This commit defines sizes for those
heading levels to keep streamed content visually stable and consistent with the
existing `h2/h3` scale.
- Add explicit `h1` sizing to match the design type scale
- Add explicit `h4` sizing to avoid browser defaults
- Keep existing serif styling and spacing intact
Ingestion was all-or-nothing and Java 25 sources still pointed at the older
API-only view plus an EA mirror. That made targeted re-ingestion slow and left
out useful release context. This adds doc-set filtering and refreshes Java 25
sources and mappings while tightening ingestion script behavior.
- Add DOCS_SETS filtering and selection logging in DocumentProcessor
- Refresh Java 25 sources (release notes + IBM/JetBrains) and remove EA mirror
- Update ingestion scripts to pass doc-set filters, select runnable JARs, and
harden progress counters
- Update docs and tests to match the new source set
Prompts implied broad Java 25/EA coverage and allowed assumptions when retrieval
was thin. That risks overconfident answers. This tightens guidance to ask for
missing context, label uncertainty, and prefer stable official sources, with
aligned docs and reranker behavior.
- Update core system prompt and related prompt variants to avoid guessing
- Require concise clarifying questions when version or toolchain details are missing
- Bias reranker toward official/stable sources
- Update prompt documentation example and guided learning template
Document IDs were raw hash strings, which are stable but inconsistent in shape.
This derives a deterministic UUID from the hash so identifiers remain stable
while using a standard UUID format.
- Add deterministic UUID creation to ContentHasher with validation
- Inject ContentHasher into DocumentFactory
- Use UUID-based IDs for hashed documents
Spotless removed an unused import and reflowed a wrapped exception in the doc-set
filter, with no behavior change.
- Remove unused import
- Align wrapped exception formatting
Java 25 queries were still surfacing Java 24 docs because the version filter
only matched `java25/jdk25` tokens and required multiple matches to filter.
Oracle URLs are `.../java/javase/25/...`, and some titles encode version too.
This tightens matching to actual Oracle URL patterns and doc titles, and
accepts any version match to prevent 24-only results.
- Match `javase/25`, `java/javase/25`, `java/se/25`, `java-25`, `jdk-25`
- Include title-based matching (e.g., “Java SE 25 & JDK 25”)
- Boost query with “Java SE 25” context
- Apply version filter when at least one match is found
System defaults still referenced Java 24 for crawl config and prompt injection.
This aligns defaults with the Java 25 toolchain and updates the canonical
Java 25 API base so ingestion and references point at the correct docs.
- Set Java 25 as default docs root and JDK version
- Update prompt default to Java 25
- Point Java 25 API base at `/docs/api/`
A corrupt embeddings cache currently breaks startup by throwing and stopping
ingestion. This moves bad cache files out of the way and logs a clear warning,
while keeping cache behavior deterministic and easy to diagnose.
- Use constants for cache filenames and timestamps
- Quarantine invalid cache files instead of crashing
- Emit warning in ingestion script if a cache file was quarantined
Default remote embedding settings were blank or pointed at a different model,
which can silently misalign ingestion and retrieval. This sets explicit defaults
aligned to the Qwen3 embedding stack.
- Set default embedding server URL to Novita’s OpenAI‑compatible endpoint
- Set default model to qwen/qwen3-embedding-8b
- Keep override via env vars intact
Spotless reflowed the timestamp formatter line only; behavior unchanged.
- Reflow timestamp formatter constant line
Spring documentation URL structures have shifted (root bases, /api/, /reference/),
while our local mirror normalization and source registry still assumed older
docs/current/... layouts and Spring AI reference/1.0/api/. That mismatch can
produce incorrect or broken source attribution links when mapping local paths to
public URLs. This updates the registry + properties to use stable base URLs and
teaches the local path mapper how to normalize legacy mirror layouts into the
current upstream structure.
- Add root base settings for Spring Boot/Framework/AI and map local prefixes to them
- Update Spring Boot/Spring AI API base defaults to current upstream locations
- Add reference-base properties for Spring docs and align URL reconstruction inputs
- Normalize legacy local mirror paths (duplicate javadoc prefixes, api/current, etc.)
into the current Spring docs URL structure
Re-ingestion currently re-parses every file even when the local doc mirror hasn’t
changed, which makes iterative runs slow and increases load on embedding + upload
pipelines. This adds a file-level fingerprint marker (size + mtime) keyed by the
authoritative URL so reruns can quickly skip unchanged files while preserving
chunk-level dedupe and citations.
- Add file-level marker read/write under app.docs.index-dir
- Skip parsing/processing when marker matches current file size + mtime
- Record marker only after a file completes ingestion successfully
Doc mirroring can leave partially-downloaded trees in place, which then looks
“present” but fails later during parsing/ingestion. This makes the fetch step
safer and more controllable by quarantining incomplete mirrors and optionally
refreshing small “quick” sources.
- Add `set -euo pipefail` and explicit flag parsing
- Quarantine incomplete mirrors under `data/docs/.quarantine/` before refetch
- Add `--include-quick` to refresh small doc mirrors on demand
- Keep behavior configurable via `--no-clean`
The default embedding dimensions and Qdrant payload-index behavior must match the
active embedding model and metadata filtering expectations; mismatches can cause
inconsistent retrieval or slow/incorrect filtered queries. This aligns defaults
and documents the incremental ingestion behavior.
- Default embedding dimensions to 4096 to match the Qwen3 embedding model
- Enable payload index ensure on boot so metadata filtering works reliably
- Document fetch quick-includes and file-level marker behavior
The new file-level “skip unchanged” optimization is only safe when a file’s
fingerprint (size/mtime) still matches what was previously ingested. When a file
changes, we must delete the old chunks/records first; otherwise Qdrant + local
parsed snapshots can retain stale content under the same URL. This adds a
deterministic prune + reindex path and improves local-file filtering for Spring
reference mirrors.
- Skip versioned Spring Framework/Spring AI reference trees under /reference/<version>/…
- Detect file fingerprint changes and prune previously ingested content before reindexing
- Use chunk-processing outcome to distinguish “all chunks already ingested” vs “no chunks generated”
- Keep file marker reads/writes robust and align formatting

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/lib/services/sse.ts (1)

85-125: ⚠️ Potential issue | 🟠 Major

Stop using fallback patterns for invalid SSE payloads.
validateWithSchema correctly logs validation failures via logZodFailure() with full context, but the SSE code ignores these failures and falls back to unvalidated data (status/error messages, text) or silently skips callbacks (citations, providers). This violates the rule against fallback paths and swallowing validation errors. When an SSE payload fails validation, it should fail fast with an error callback, not degrade to raw eventData.

🛠️ Example fix pattern
 if (normalizedType === SSE_EVENT_STATUS) {
const parsed = tryParseJson(eventData, source)
const validated = validateWithSchema(StreamStatusSchema, parsed, `${source}:status`)
- callbacks.onStatus?.(validated.success ? validated.validated : { message: eventData })+ if (!validated.success) {+ callbacks.onError?.({ message: 'Invalid stream status payload' })+ return+ }+ callbacks.onStatus?.(validated.validated)
return
}

Apply similarly to error, citation, provider, and text branches.

🤖 Fix all issues with AI agents
In `@frontend/src/lib/services/streamRecovery.ts`:
- Around line 86-104: The top-of-file doc comments for buildStreamRetryStatus
and buildStreamRecoverySucceededStatus merely restate the function names; remove
or replace them with short "why" style comments (or delete entirely) that
explain the UX intent only (e.g., "Shown to users when streaming temporarily
fails to indicate a retry" for buildStreamRetryStatus and "Shown when a retry
succeeds and streaming resumes" for buildStreamRecoverySucceededStatus) so the
code remains self-documenting while preserving rationale; locate the comments
above the functions buildStreamRetryStatus and
buildStreamRecoverySucceededStatus and update or remove them accordingly.
In `@scripts/lib/github_identity.sh`:
- Around line 91-96: The CANONICAL_COLLECTION_NAME assignment can silently
produce malformed names if encode_collection_segment fails; change the code to
capture each encoded segment into temporary vars (e.g., owner_enc and name_enc)
by calling encode_collection_segment "$REPOSITORY_OWNER" and
encode_collection_segment "$REPOSITORY_NAME", check their exit status and/or
that they are non-empty, and if either call failed log an error and exit
non‑zero instead of continuing; only build CANONICAL_COLLECTION_NAME from the
validated owner_enc and name_enc values to ensure no empty segments are used.
- Line 212: The condition uses an unset-sensitive parameter expansion (if [ -n
"$REPO_URL" ]; then) which will fail under set -u; change the test to use a safe
default expansion such as ${REPO_URL:-} so it reads like the existing if but
with ${REPO_URL:-} to avoid nounset errors when REPO_URL is undefined (update
the if condition referencing REPO_URL accordingly).
In `@src/main/java/com/williamcallahan/javachat/service/HybridSearchService.java`:
- Around line 257-261: In the hashCode method, rename the generic local variable
`result` to an intent-revealing name such as `hash`: update the declaration and
subsequent usage so the computation uses `int hash =
java.util.Objects.hash(sparseVector, retrievalFilter);` and `hash = 31 * hash +
java.util.Arrays.hashCode(denseVector);` (ensure the method still returns the
renamed variable) so references to `sparseVector`, `retrievalFilter`, and
`denseVector` remain unchanged.
- Around line 164-176: Replace the parallel-list fan-out (collectionNames +
futures) with a single Map<String, CompletableFuture<List<ScoredPoint>>> that
maps each collection name to its future (created via
buildHybridQueryRequest(...),
QdrantListenableFutureBridge.toCompletableFuture(...) and
qdrantClient.queryAsync(...)); then refactor collectFanOutResults to accept that
map plus the existing scoredPointsByUuid and collectionFailures (reducing the
positional parameters below five) and update the other occurrence around lines
305-339 similarly so both call sites and the collectFanOutResults signature use
the map-based input to avoid index coupling.
- Line 393: Rename the private record ScoredResult to a domain-specific name
(e.g., ScoredPointMatch) and update all references to it (constructor usages,
variable declarations, return types, and pattern matches) to the new identifier;
ensure the declaration line `private record ScoredResult(String id, double
score, ScoredPoint point, String collection)` is changed to `private record
ScoredPointMatch(...)` and adjust any code in methods that construct or consume
ScoredResult so compilation and intent-revealing naming are preserved.
- Around line 53-99: The HybridSearchService constructor currently accepts six
parameters; refactor it to accept a single QueryEncodingServices parameter
instead of the three encoding params so the constructor signature becomes four
arguments (QdrantClient, QueryEncodingServices, AppProperties,
Optional<QdrantGitHubCollectionDiscovery>) and update field assignment to use
the injected QueryEncodingServices directly; also register/provide a
QueryEncodingServices bean (or factory) where the service is wired so callers
supply the composed record (constructed from EmbeddingClient,
LexicalSparseVectorEncoder, QdrantRetrievalConstraintBuilder) rather than
passing those three individually.
In `@src/main/java/com/williamcallahan/javachat/service/HybridVectorService.java`:
- Around line 184-205: Extract the literal "url" into a single named constant
(e.g. private static final String URL_PAYLOAD_KEY = "url") in
HybridVectorService and replace all inline occurrences — the List.of("url") in
ScrollPoints building, the retrievedPoint.getPayloadMap().get("url") lookup, and
any other usages around lines 308-325 — with that constant; ensure
imports/visibility match the class scope and run tests to confirm no behavior
change.
🧹 Nitpick comments (2)
src/main/java/com/williamcallahan/javachat/service/GuidedLearningService.java (2)

141-159: Consider skipping enrichment when no book docs.
If bookDocuments is empty, returning emptyEnrichment() avoids ungrounded hints and saves the extra enrichment call. Tiny fun: no ingredients, no recipe.

♻️ Suggested short‑circuit
 List<Document> retrievedDocuments = retrievalService.retrieve(query);
List<Document> bookDocuments = filterToBook(retrievedDocuments);
+ if (bookDocuments.isEmpty()) {+ return emptyEnrichment();+ }
List<String> snippets = bookDocuments.stream()

236-273: Cache check could reduce repeat generation.
If the cache is intended to save LLM work, consider checking getCachedLessonMarkdown(slug) before starting retrieval/streaming. Tiny fun: reheating leftovers beats cooking again.

Comment threadfrontend/src/lib/services/streamRecovery.ts
Comment threadscripts/lib/github_identity.sh
Comment threadscripts/lib/github_identity.sh Outdated
Comment threadsrc/main/java/com/williamcallahan/javachat/service/HybridSearchService.java Outdated
Comment threadsrc/main/java/com/williamcallahan/javachat/service/HybridSearchService.java Outdated
Comment threadsrc/main/java/com/williamcallahan/javachat/service/HybridSearchService.java Outdated
Comment threadsrc/main/java/com/williamcallahan/javachat/service/HybridSearchService.java Outdated
Comment threadsrc/main/java/com/williamcallahan/javachat/service/HybridVectorService.java Outdated

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/main/java/com/williamcallahan/javachat/service/HybridVectorService.java`:
- Around line 270-301: In doUpsert, add defensive checks after calling
EmbeddingBatchEmbedder.embedDocuments to ensure embeddings is non-null, has the
same size as documents, and that no embedding entry is null before building
points; if sizes mismatch or nulls are found, either filter out the
corresponding Document(s) (and log which were dropped) or throw a clear
IllegalStateException so the upsert is aborted. Specifically validate the
embeddings list returned by embeddingClient via
EmbeddingBatchEmbedder.embedDocuments, adjust the loop that builds points
(HybridVectorPointFactory.HybridVectorSet / HybridVectorPointFactory.buildPoint)
to only process aligned pairs, and avoid calling qdrantClient.upsertAsync with
mismatched counts.
🧹 Nitpick comments (1)
src/main/java/com/williamcallahan/javachat/service/HybridVectorService.java (1)

178-189: Optional cleanup: Objects.requireNonNull(List.of(...)) is redundant.
Tiny tidbit: removing tiny redundancies keeps the hot path extra crisp.

♻️ Proposed tidy-up
- List<String> urlPayloadFields = Objects.requireNonNull(List.of(URL_PAYLOAD_FIELD), "urlPayloadFields");+ List<String> urlPayloadFields = List.of(URL_PAYLOAD_FIELD);

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationenhancementNew feature or requestjavaPull requests that update java coderefactorCode refactoring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@WilliamAGH