5 Commits

7 changed files with 154 additions and 102 deletions

View File

@@ -8,11 +8,15 @@ In kongruity, the artifacts become "sticky notes." A board full of them looks ch
With a click, they are semantically evaluated, grouped into thematic clusters with descriptive headers, rankable and exportable to project planning and execution tools. With a click, they are semantically evaluated, grouped into thematic clusters with descriptive headers, rankable and exportable to project planning and execution tools.
## Voyage AI voyage-3.5
![Embedding model benchmarking.](Voyage.jpg)
## Clustering and evaluation: methodology ## Clustering and evaluation: methodology
Two models run in parallel, and neither sees the other's work. Anthropic's `claude-sonnet-5` (`backend/services/clustering.service.js`) reads the raw text of every note and groups them into labeled thematic clusters. Two models run in parallel, and neither sees the other's work. Anthropic's `claude-sonnet-5` (`backend/services/clustering.service.js`) reads the raw text of every note and groups them into labeled thematic clusters.
At the same time, Voyage AI's voyage-3 model (`backend/services/embedding.service.js`) converts each note's text into a numeric representation of its semantic meaning aka vector. At the same time, Voyage AI's voyage-3.5 model (`backend/services/embedding.service.js`) converts each note's text into a numeric representation of its semantic meaning aka vector.
Once the LLM returns, kongruity scores that grouping (`backend/services/validation.service.js`) using an established silhouette coefficient, with cosine distance rather than Euclidean as the proximity metric. Once the LLM returns, kongruity scores that grouping (`backend/services/validation.service.js`) using an established silhouette coefficient, with cosine distance rather than Euclidean as the proximity metric.

BIN
Voyage.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

42
agents.md Normal file
View File

@@ -0,0 +1,42 @@
# AI Agent Instructions: Fullstack Vite 7 (React) + Express + TypeScript + npm
You are an expert AI fullstack software engineer specialized in Vite 7, React, Express, TypeScript, and modern web architectures. Follow these rules strictly when modifying this codebase.
## 1. Project Structure & Context
* **Frontend:** React SPA powered by Vite 7.x (Entry: `src/main.tsx` or client folder).
* **Backend:** Express Node.js application (Server entry: `server.ts` or server folder).
* **Package Manager:** npm (`package-lock.json` is the strict source of truth).
* **TypeScript Setup:** Strict Mode enabled independently across both environments.
## 2. Express Backend TypeScript Rules
* **Typed Request/Response:** Explicitly type Express route handlers using native Express types:
```typescript
import { Request, Response, NextFunction } from 'express';
// Example for typed request bodies/params:
interface CreateUserBody { username: string; }
app.post('/user', (req: Request<{}, {}, CreateUserBody>, res: Response) => { ... });
```
* **Async Error Catching:** Always wrap async middleware/route handlers in `try/catch` and pass errors to `next(err)`. Do not let unhandled promise rejections crash the Node process.
* **Shared Types:** If frontend and backend share types (e.g., API payloads, User models), place them in a shared directory or export them cleanly from the backend to prevent duplicating code.
## 3. Frontend React + Vite Rules
* **Component Typings:** Use standard type inference or explicit return types (`function Component(): React.JSX.Element`). Avoid the legacy `React.FC`.
* **Strict Prop Types:** Every component must have an explicitly typed `interface` or `type` for its props. No implicit `any`.
* **Event Handlers:** Use exact React synthetic event types (e.g., `React.ChangeEvent<HTMLInputElement>`) instead of generic native events.
* **File Extensions:** Use `.tsx` exclusively for files containing JSX. Use `.ts` strictly for pure logic, hooks, or type definitions.
## 4. Strict Code Quality & Native Guards
* **No `any`:** Never use `any`. Use `unknown` for unpredictable runtime data (like Express `req.body` or frontend `fetch` payloads).
* **No Validation Libraries:** Do not install Zod, TypeBox, or Yup. Write explicit, manual type predicate functions (`function isUser(obj: any): obj is User`) to safely validate runtime data incoming to both the server and client.
* **No Enums:** Avoid TypeScript `enum`. Use string-literal unions (`type Status = 'active' | 'pending'`) or `const StatusEnum = { ... } as const`.
## 5. Verification & Workflow Commands
Before declaring a task complete, you must verify both environments compile flawlessly via npm:
* **Install Dependencies:** `npm install`
* **Type-Check Project:** Run the designated workspace or folder type-checking scripts (e.g., `npm run type-check` or `npx tsc --noEmit` across both roots).
* **Build Verification:** Run production build scripts (e.g., `npm run build`) to ensure both Express asset compilation and Vite bundling pass without error.
## 6. How to Respond
* **Verify Types First:** Run type-checking commands automatically after modifying files to capture compilation breaks before presenting the solution.
* **Targeted Diffs:** Provide concise, targeted updates. Do not rewrite whole files if only a few lines change.
* **Self-Correct:** If a build command fails, read the compiler/Vite/Node logs, fix the root cause, and re-test before asking the user for help.

View File

@@ -27,7 +27,7 @@ export const embedNotes = async (notes) => {
for await (const chunk of batches) { for await (const chunk of batches) {
const response = await client.embed({ const response = await client.embed({
input: chunk.map((n) => n.text), input: chunk.map((n) => n.text),
model: "voyage-3", model: "voyage-3.5",
}); });
response.data.forEach((item, i) => { response.data.forEach((item, i) => {

View File

@@ -1,40 +1,39 @@
{ {
"name": "congruity-frontend", "name": "kongruity-frontend",
"private": true, "private": true,
"version": "0.1.0", "version": "0.1.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc -b && vite build", "build": "tsc -b && vite build",
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview", "preview": "vite preview",
"test": "vitest run --config vitest.config.ts", "test": "vitest run --config vitest.config.ts",
"test:watch": "vitest --config vitest.config.ts" "test:watch": "vitest --config vitest.config.ts"
}, },
"dependencies": { "dependencies": {
"@tanstack/react-query": "^5.90.21", "@tanstack/react-query": "^5.90.21",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-router-dom": "^7.13.0" "react-router-dom": "^7.13.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.1", "@eslint/js": "^9.39.1",
"@testing-library/jest-dom": "^6.9.1", "@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2", "@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1", "@testing-library/user-event": "^14.6.1",
"@types/node": "^24.10.1", "@types/node": "^24.10.1",
"@types/react": "^19.2.7", "@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1", "@vitejs/plugin-react": "^5.1.1",
"eslint": "^9.39.1", "eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24", "eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0", "globals": "^16.5.0",
"jsdom": "^28.0.0", "jsdom": "^28.0.0",
"typescript": "~5.9.3", "typescript": "~5.9.3",
"typescript-eslint": "^8.48.0", "typescript-eslint": "^8.48.0",
"vite": "^7.3.1", "vite": "^7.3.1",
"vitest": "^4.0.18" "vitest": "^4.0.18"
} }
} }

View File

@@ -95,7 +95,7 @@ const Stickies = () => {
<div className="clusters-container"> <div className="clusters-container">
{score != null && ( {score != null && (
<div className="cohesion-score"> <div className="cohesion-score">
Cluster cohesion: {scoreLabel(score)} Cluster cohesion: <strong>{scoreLabel(score)}</strong>
</div> </div>
)} )}
{rankedClusters.map((group, index) => ( {rankedClusters.map((group, index) => (

View File

@@ -1,100 +1,107 @@
.stickies-grid { .stickies-grid {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 16px; gap: 16px;
justify-content: center; justify-content: center;
margin-top: 18px; margin-top: 18px;
padding: 24px; padding: 24px;
} }
.stickies-container { .stickies-container {
margin: 36px 0px 36px 0px; margin: 36px 0px 36px 0px;
} }
.clusters-container { .clusters-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 32px; gap: 32px;
padding: 24px 0; padding: 24px 0;
} }
.cluster-group { .cluster-group {
border: 1px solid #6dd6f4; border: 1px solid #6dd6f4;
border-radius: 8px; border-radius: 8px;
padding: 16px; padding: 16px;
} }
.cluster-draggable { .cluster-draggable {
cursor: grab; cursor: grab;
transition: box-shadow 0.2s ease, border-color 0.2s ease, transform 0.15s ease; transition: box-shadow 0.2s ease, border-color 0.2s ease, transform 0.15s ease;
} }
.cluster-draggable:active { .cluster-draggable:active {
cursor: grabbing; cursor: grabbing;
} }
.cluster-drag-over { .cluster-drag-over {
border-color: #ffb7ce; border-color: #ffb7ce;
box-shadow: 0 0 12px rgba(255, 183, 206, 0.4); box-shadow: 0 0 12px rgba(255, 183, 206, 0.4);
transform: scale(1.01); transform: scale(1.01);
} }
.cluster-header { .cluster-header {
display: flex; position: relative;
align-items: center; display: flex;
gap: 12px; align-items: center;
margin-bottom: 16px; gap: 12px;
margin-bottom: 16px;
min-height: 32px;
} }
.cluster-rank { .cluster-rank {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 32px; width: 32px;
height: 32px; height: 32px;
border-radius: 50%; border-radius: 50%;
background: #6dd6f4; background: #6dd6f4;
color: #1a1a2e; color: #1a1a2e;
font-weight: 700; font-weight: 700;
font-size: 0.95em; font-size: 0.95em;
flex-shrink: 0; flex-shrink: 0;
} }
.cluster-reorder-hint { .cluster-reorder-hint {
font-size: 0.8em; font-size: 0.8em;
color: #9ca3af; color: #9ca3af;
font-style: italic; font-style: italic;
white-space: nowrap; white-space: nowrap;
flex-shrink: 0; flex-shrink: 0;
} }
.cluster-label { .cluster-label {
margin: 0; position: absolute;
font-size: 1.2em; left: 50%;
font-weight: 600; transform: translateX(-50%);
flex: 1; max-width: 50%;
margin: 0;
font-size: 1.2em;
font-weight: 600;
pointer-events: none;
} }
.cluster-drag-handle { .cluster-drag-handle {
font-size: 1.4em; margin-left: auto;
color: #6dd6f4; font-size: 1.4em;
opacity: 0.4; color: #6dd6f4;
user-select: none; opacity: 0.4;
transition: opacity 0.2s ease; user-select: none;
flex-shrink: 0; transition: opacity 0.2s ease;
flex-shrink: 0;
} }
.cluster-draggable:hover .cluster-drag-handle { .cluster-draggable:hover .cluster-drag-handle {
opacity: 0.8; opacity: 0.8;
} }
.cohesion-score { .cohesion-score {
text-align: center; text-align: center;
font-size: 0.95em; font-size: 0.95em;
color: #e0e0e0; color: #e0e0e0;
padding: 8px 16px; padding: 8px 16px;
background: rgba(109, 214, 244, 0.1); background: rgba(109, 214, 244, 0.1);
border-radius: 6px; border-radius: 6px;
width: fit-content; width: fit-content;
margin: 0 auto; margin: 0 auto;
} }