7 Commits

Author SHA1 Message Date
KS Jannette
362a47f88a Upgraded embedding model to voyage-3.5, updated README.md 2026-08-01 05:35:06 -04:00
KS Jannette
c6b07ccb56 fix minor css issue on stickies 2026-08-01 04:50:40 -04:00
KS Jannette
8e79e78006 hotfix 2026-08-01 04:41:31 -04:00
19d4a82c90 Merge pull request 'Re-aligned heuristic, updated readme, added code comment' (#3) from BUG-cohesion-scoreUI-display into master
Reviewed-on: #3
2026-08-01 08:13:29 +00:00
edeadd547a Update README.md 2026-08-01 08:11:10 +00:00
3b707a150b Update README.md 2026-08-01 08:08:34 +00:00
c2d40fa409 Update README.md 2026-08-01 08:08:19 +00:00
5 changed files with 77 additions and 66 deletions

View File

@@ -1,4 +1,4 @@
# kongruity # kongruity: Signal from noise
“...All those moments will be lost in time, like tears in rain.” “...All those moments will be lost in time, like tears in rain.”
@@ -8,15 +8,19 @@ 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.
For each note, it weighs the average distance to the other notes in its own cluster against the average distance to the notes in the nearest neighboring cluster. Averaged across every note, this yields a single cohesion score in the range [−1, 1], displayed at the top of the results. For each note, it weighs the average distance to the other notes in its own cluster against the average distance to the notes in the nearest neighboring cluster. Averaged across every note, this yields a single numeric cohesion score, displayed at the top of the results, along with plaintext: Strong, Moderate, Weak, Poor.
This yields an empirical groundedness evaluation. One model proposes the grouping; an independent model evaluates grouping accuracy. This yields an empirical groundedness evaluation. One model proposes the grouping; an independent model evaluates grouping accuracy.

BIN
Voyage.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

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

@@ -87,7 +87,7 @@ const Stickies = () => {
dragIndex.current = null; dragIndex.current = null;
setDragOverIndex(null); setDragOverIndex(null);
}; };
console.log(score?.toFixed(2))
return ( return (
<div className="stickies-container"> <div className="stickies-container">
<Button onClick={handleCluster} isLoading={isPending} label="Group Stickies By Topic" /> <Button onClick={handleCluster} isLoading={isPending} label="Group Stickies By Topic" />
@@ -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: <strong>{score.toFixed(2)}</strong> — {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;
} }