Harden lineage note indicator semantics

This commit is contained in:
Himanshu Dongre 2026-04-13 20:40:30 +05:30
parent f8e5012f64
commit 91d93becfa
2 changed files with 74 additions and 11 deletions

View file

@ -65,6 +65,16 @@ def _fork(client, space_id, checkpoint_id, branch_name="", provider="", model=""
return r
def _add_note(client, checkpoint_id, text, author="founder", kind="note"):
r = client.post(f"/api/v5/checkpoint/{checkpoint_id}/notes", json={
"text": text,
"author": author,
"kind": kind,
})
assert r.status_code == 201, r.text
return r.json()
# ── Fork tests ────────────────────────────────────────────────────────────────
def test_fork_creates_new_session(client):
@ -357,6 +367,29 @@ def test_lineage_checkpoint_includes_author_agent(client):
)
def test_lineage_checkpoint_includes_note_summary(client):
"""note_count and note_kinds round-trip through the lineage endpoint."""
repo_id = _create_repo(client, "Lineage Note Summary")
session_id = _create_session(client, repo_id)
mixed = _commit(client, repo_id, session_id, message="Checkpoint with mixed notes")
plain = _commit(client, repo_id, session_id, message="Checkpoint without notes")
_add_note(client, mixed["id"], "Milestone note", kind="milestone")
_add_note(client, mixed["id"], "Noise note", kind="noise")
_add_note(client, mixed["id"], "Plain note", kind="note")
r = client.get(f"/api/v5/lineage/spaces/{repo_id}")
assert r.status_code == 200, r.text
checkpoints = r.json()["checkpoints"]
by_id = {c["id"]: c for c in checkpoints}
assert by_id[mixed["id"]]["note_count"] == 3
assert by_id[mixed["id"]]["note_kinds"] == ["milestone", "noise", "note"]
assert by_id[plain["id"]]["note_count"] == 0
assert by_id[plain["id"]]["note_kinds"] == []
# ── Compare tests ─────────────────────────────────────────────────────────────
def test_compare_same_checkpoint(client):

View file

@ -38,6 +38,42 @@ function fmt(d: string) {
});
}
function getCheckpointNoteBadge(checkpoint: CheckpointNode) {
const kinds = checkpoint.note_kinds ?? [];
const plural = checkpoint.note_count !== 1 ? 's' : '';
const kindSummary = kinds.length > 0 ? kinds.join(' + ') : 'note';
if (kinds.length > 1) {
return {
label: 'mix',
title: `${checkpoint.note_count} note${plural} · ${kindSummary}`,
className: 'text-sky-300 bg-sky-950/30 border border-sky-500/30',
};
}
if (kinds.includes('milestone')) {
return {
label: '★',
title: `${checkpoint.note_count} note${plural} · milestone`,
className: 'text-amber-400 bg-amber-900/20 border border-amber-500/30',
};
}
if (kinds.includes('noise')) {
return {
label: '◌',
title: `${checkpoint.note_count} note${plural} · noise`,
className: 'text-gray-500 bg-gray-800/30 border border-gray-700',
};
}
return {
label: '●',
title: `${checkpoint.note_count} note${plural} · note`,
className: 'text-gray-400 bg-gray-800/30 border border-gray-700',
};
}
// ── Compare Panel ─────────────────────────────────────────────────────────────
function ComparePanel({
@ -296,6 +332,7 @@ function CheckpointCard({
onFork: (c: CheckpointNode) => void;
}) {
const [expanded, setExpanded] = useState(false);
const noteBadge = checkpoint.note_count > 0 ? getCheckpointNoteBadge(checkpoint) : null;
return (
<div
@ -320,19 +357,12 @@ function CheckpointCard({
</span>
)}
<span className="text-[10px] text-gray-600">{fmt(checkpoint.created_at)}</span>
{checkpoint.note_count > 0 && (
{noteBadge && (
<span
className={`text-[9px] font-medium px-1 py-px rounded ${
checkpoint.note_kinds.includes('milestone')
? 'text-amber-400 bg-amber-900/20 border border-amber-500/30'
: checkpoint.note_kinds.includes('noise')
? 'text-gray-500 bg-gray-800/30 border border-gray-700'
: 'text-gray-400 bg-gray-800/30 border border-gray-700'
}`}
title={`${checkpoint.note_count} note${checkpoint.note_count !== 1 ? 's' : ''}`}
className={`text-[9px] font-medium px-1 py-px rounded ${noteBadge.className}`}
title={noteBadge.title}
>
{checkpoint.note_kinds.includes('milestone') ? '★' :
checkpoint.note_kinds.includes('noise') ? '◌' : '●'}
{noteBadge.label}
</span>
)}
</div>