tink-harness 1.9.6 → 1.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tink",
3
3
  "description": "A small harness layer for Claude Code and Codex.",
4
- "version": "1.9.6",
4
+ "version": "1.9.7",
5
5
  "author": {
6
6
  "name": "dotori"
7
7
  }
package/CHANGELOG.md CHANGED
@@ -6,6 +6,17 @@ All notable changes to Tink are tracked here.
6
6
 
7
7
  No unreleased changes yet.
8
8
 
9
+ ## [1.9.7] - 2026-06-10
10
+
11
+ ### Added
12
+
13
+ - The harness map now looks like a planetary system: usage/evidence/score satellites orbit their harness slowly (28-63s per revolution, alternating directions) along dashed orbit rings, top harnesses get a Saturn-style ring, and a starfield with twinkling stars sits behind the graph.
14
+ - Selecting a node now dims and blurs everything unrelated (focus mode); clicking the background clears the selection and restores the detail panel.
15
+
16
+ ### Fixed
17
+
18
+ - Dragging the map no longer triggers text selection on labels and captions (user-select disabled on the map panel).
19
+
9
20
  ## [1.9.6] - 2026-06-10
10
21
 
11
22
  ### Added
package/VERSIONING.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Versioning
2
2
 
3
- Current version: `1.9.6`
3
+ Current version: `1.9.7`
4
4
 
5
5
  Tink follows semver from `1.0.0` onward.
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tink-harness",
3
- "version": "1.9.6",
3
+ "version": "1.9.7",
4
4
  "description": "Self-growing harnesses for Claude Code and Codex.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -618,8 +618,7 @@ function buildGraphLayout(summary) {
618
618
  glow: score >= 50 || radius >= 20
619
619
  };
620
620
  });
621
- const augmented = [...positioned];
622
- const virtualEdges = [];
621
+ const orbitSystems = [];
623
622
  for (const node of positioned.filter((item) => item.type === 'harness')) {
624
623
  const harnessId = shortLabel(node.id);
625
624
  const harness = harnessById.get(harnessId);
@@ -627,38 +626,25 @@ function buildGraphLayout(summary) {
627
626
  const uses = Number(harness.signals?.uses || 0);
628
627
  const evidenceCount = (harness.evidence_handles || []).length;
629
628
  const factorCount = (harness.candidate_score?.factors || []).length;
630
- const satellites = [
631
- ...Array.from({ length: Math.min(14, Math.ceil(uses / 2)) }, (_, index) => ({ kind: 'signal', index, total: Math.min(14, Math.ceil(uses / 2)), radius: 2.8 + Math.min(3.5, uses / 12) })),
632
- ...Array.from({ length: Math.min(6, evidenceCount) }, (_, index) => ({ kind: 'evidence', index, total: Math.min(6, evidenceCount), radius: 3.5 })),
633
- ...Array.from({ length: Math.min(5, factorCount) }, (_, index) => ({ kind: 'score', index, total: Math.min(5, factorCount), radius: 3.8 }))
634
- ];
635
- satellites.forEach((satellite, offset) => {
636
- const seed = hashString(`${node.id}:${satellite.kind}:${satellite.index}`);
637
- const angle = ((seed % 6283) / 1000) + offset * 0.45;
638
- const distance = node.radius + 24 + (seed % 42);
639
- const child = {
640
- id: `${satellite.kind}:${harnessId}:${satellite.index}`,
641
- type: satellite.kind,
642
- label: satellite.kind,
643
- weight: 1,
644
- x: clamp(node.x + Math.cos(angle) * distance, 25, 1065),
645
- y: clamp(node.y + Math.sin(angle) * distance, 25, 655),
646
- radius: satellite.radius,
647
- color: TYPE_COLORS[satellite.kind] || TYPE_COLORS.unknown,
648
- glow: satellite.kind === 'score'
649
- };
650
- augmented.push(child);
651
- virtualEdges.push({
652
- source: node.id,
653
- target: child.id,
654
- type: satellite.kind,
655
- count: 1
656
- });
657
- });
629
+ const seed = hashString(node.id);
630
+ const rings = [
631
+ { kind: 'signal', count: Math.min(10, Math.ceil(uses / 2)), distance: node.radius + 18, dotRadius: 2.4 },
632
+ { kind: 'evidence', count: Math.min(6, evidenceCount), distance: node.radius + 29, dotRadius: 2.9 },
633
+ { kind: 'score', count: Math.min(5, factorCount), distance: node.radius + 40, dotRadius: 3.2 }
634
+ ]
635
+ .filter((ring) => ring.count > 0)
636
+ .map((ring, ringIndex) => ({
637
+ ...ring,
638
+ duration: (28 + ((seed >> (ringIndex * 3)) % 36)).toFixed(0),
639
+ reverse: ringIndex % 2 === 1,
640
+ phase: ((seed >> (ringIndex * 5)) % 628) / 100
641
+ }));
642
+ if (rings.length) {
643
+ orbitSystems.push({ parentId: node.id, x: node.x, y: node.y, rings });
644
+ }
658
645
  }
659
- const byId = new Map(augmented.map((node) => [node.id, node]));
660
- const filteredEdges = getRenderableEdges(edges);
661
- const drawnEdges = [...filteredEdges, ...virtualEdges]
646
+ const byId = new Map(positioned.map((node) => [node.id, node]));
647
+ const drawnEdges = getRenderableEdges(edges)
662
648
  .map((edge) => ({
663
649
  ...edge,
664
650
  sourceNode: byId.get(edge.source),
@@ -667,15 +653,26 @@ function buildGraphLayout(summary) {
667
653
  .filter((edge) => edge.sourceNode && edge.targetNode)
668
654
  .slice(0, 240);
669
655
 
670
- return { nodes: augmented, edges: drawnEdges };
656
+ return { nodes: positioned, edges: drawnEdges, orbitSystems };
671
657
  }
672
658
 
673
659
  function renderGraphCanvas(summary, copy) {
674
- const { nodes, edges } = buildGraphLayout(summary);
660
+ const { nodes, edges, orbitSystems } = buildGraphLayout(summary);
675
661
  const strongest = nodes
676
662
  .filter((node) => node.type === 'harness')
677
663
  .sort((a, b) => Number(b.weight || 0) - Number(a.weight || 0))
678
664
  .slice(0, 8);
665
+ const stars = Array.from({ length: 110 }, (_, index) => {
666
+ const seed = hashString(`star:${index}`);
667
+ return {
668
+ x: seed % 1090,
669
+ y: (seed >> 4) % 680,
670
+ r: (0.5 + ((seed >> 8) % 12) / 10).toFixed(1),
671
+ opacity: (0.1 + ((seed >> 6) % 45) / 100).toFixed(2),
672
+ twinkle: index % 4 === 0,
673
+ delay: seed % 5000
674
+ };
675
+ });
679
676
  const mapTitle = copy.knowledgeGraph || copy.harnessMap || 'Harness map';
680
677
  const mapEyebrow = copy.harnessMap && copy.harnessMap !== mapTitle ? copy.harnessMap : '';
681
678
  return `
@@ -700,9 +697,10 @@ function renderGraphCanvas(summary, copy) {
700
697
  </div>
701
698
  <svg class="graph-canvas" viewBox="0 0 1090 680" role="img" aria-label="Harness health graph">
702
699
  <defs>
703
- <radialGradient id="graph-bg-grad" cx="50%" cy="42%" r="75%">
704
- <stop offset="0%" style="stop-color: #16181D"/>
705
- <stop offset="100%" style="stop-color: #0C0D10"/>
700
+ <radialGradient id="graph-bg-grad" cx="50%" cy="42%" r="80%">
701
+ <stop offset="0%" style="stop-color: #11141C"/>
702
+ <stop offset="60%" style="stop-color: #0A0C12"/>
703
+ <stop offset="100%" style="stop-color: #06070B"/>
706
704
  </radialGradient>
707
705
  ${Object.entries(TYPE_COLORS).map(([type, color]) => `
708
706
  <radialGradient id="node-grad-${escapeAttr(type)}" cx="32%" cy="28%" r="78%">
@@ -713,6 +711,11 @@ function renderGraphCanvas(summary, copy) {
713
711
  `).join('')}
714
712
  </defs>
715
713
  <rect class="graph-bg" width="1090" height="680" fill="url(#graph-bg-grad)"/>
714
+ <g class="starfield" aria-hidden="true">
715
+ ${stars.map((star) => `
716
+ <circle cx="${star.x}" cy="${star.y}" r="${star.r}" fill="#FFFFFF" opacity="${star.opacity}"${star.twinkle ? ` class="star-twinkle" style="--twinkle-delay: ${star.delay}ms"` : ''}/>
717
+ `).join('')}
718
+ </g>
716
719
  <g id="graph-viewport">
717
720
  <g class="edges">
718
721
  ${edges.map((edge, index) => `
@@ -731,6 +734,22 @@ function renderGraphCanvas(summary, copy) {
731
734
  />
732
735
  `).join('')}
733
736
  </g>
737
+ <g class="orbits" aria-hidden="true">
738
+ ${orbitSystems.map((system) => `
739
+ <g class="orbit-system" data-parent="${escapeAttr(system.parentId)}" transform="translate(${system.x.toFixed(1)} ${system.y.toFixed(1)})">
740
+ ${system.rings.map((ring) => {
741
+ const dots = Array.from({ length: ring.count }, (_, dotIndex) => {
742
+ const angle = ring.phase + (dotIndex * Math.PI * 2) / ring.count;
743
+ return `<circle class="orbit-dot" cx="${(Math.cos(angle) * ring.distance).toFixed(1)}" cy="${(Math.sin(angle) * ring.distance).toFixed(1)}" r="${ring.dotRadius}" fill="url(#node-grad-${escapeAttr(ring.kind)})"/>`;
744
+ }).join('');
745
+ return `
746
+ <circle class="orbit-ring" cx="0" cy="0" r="${ring.distance.toFixed(1)}"/>
747
+ <g class="orbit-spin${ring.reverse ? ' is-reverse' : ''}" style="--orbit-dur: ${ring.duration}s">${dots}</g>
748
+ `;
749
+ }).join('')}
750
+ </g>
751
+ `).join('')}
752
+ </g>
734
753
  <g class="nodes">
735
754
  ${nodes.map((node, index) => {
736
755
  const seed = hashString(node.id);
@@ -740,6 +759,9 @@ function renderGraphCanvas(summary, copy) {
740
759
  const floatY = (((seed >> 3) % 7) - 3).toFixed(1);
741
760
  return `
742
761
  <g class="node-float" style="--float-dur: ${floatDuration}s; --float-delay: ${floatDelay}ms; --float-x: ${floatX}px; --float-y: ${floatY}px">
762
+ ${node.type === 'harness' && node.radius >= 15 ? `
763
+ <ellipse class="planet-ring" cx="${node.x.toFixed(1)}" cy="${node.y.toFixed(1)}" rx="${(node.radius * 1.75).toFixed(1)}" ry="${(node.radius * 0.5).toFixed(1)}" transform="rotate(-16 ${node.x.toFixed(1)} ${node.y.toFixed(1)})"/>
764
+ ` : ''}
743
765
  <circle
744
766
  class="graph-node ${node.type === 'harness' ? 'is-interactive' : ''}"
745
767
  style="--enter-delay: ${Math.min(index * 9, 1100)}ms"
@@ -1358,14 +1380,30 @@ function renderScript(harnesses, copy) {
1358
1380
  const setFilterStatus = (value) => {
1359
1381
  if (filterStatus) filterStatus.textContent = value;
1360
1382
  };
1383
+ const graphCanvas = document.querySelector('.graph-canvas');
1384
+ const orbitSystems = Array.from(document.querySelectorAll('.orbit-system'));
1385
+ const defaultSelectedPanel = selectedPanel ? selectedPanel.innerHTML : '';
1386
+ function syncOrbits() {
1387
+ const hasSelection = graphCanvas && graphCanvas.classList.contains('has-selection');
1388
+ orbitSystems.forEach((system) => {
1389
+ const parent = nodeById(system.dataset.parent);
1390
+ const hidden = !parent || parent.classList.contains('is-hidden') || parent.classList.contains('is-filtered-out');
1391
+ system.classList.toggle('is-hidden', hidden);
1392
+ const related = parent && (parent.classList.contains('is-selected') || parent.classList.contains('is-related'));
1393
+ system.classList.toggle('is-dimmed', Boolean(hasSelection && !related));
1394
+ });
1395
+ }
1361
1396
  function clearSelection() {
1362
1397
  nodes.forEach((item) => item.classList.remove('is-selected', 'is-related'));
1363
1398
  edges.forEach((item) => item.classList.remove('is-related'));
1364
1399
  cards.forEach((item) => item.classList.remove('is-selected'));
1400
+ if (graphCanvas) graphCanvas.classList.remove('has-selection');
1401
+ syncOrbits();
1365
1402
  }
1366
1403
  function selectNode(node) {
1367
1404
  clearSelection();
1368
1405
  node.classList.add('is-selected');
1406
+ if (graphCanvas) graphCanvas.classList.add('has-selection');
1369
1407
  const id = node.dataset.nodeId;
1370
1408
  const item = byHarnessId.get(id);
1371
1409
  edges.forEach((edge) => {
@@ -1393,6 +1431,7 @@ function renderScript(harnesses, copy) {
1393
1431
  } else if (selectedPanel) {
1394
1432
  selectedPanel.innerHTML = '<p class="eyebrow">' + esc(copy.selected) + '</p><h2>' + esc(node.dataset.nodeLabel) + '</h2><p>' + esc(id) + '</p><dl><div><dt>' + esc(copy.type) + '</dt><dd>' + esc(displayValue(node.dataset.nodeType)) + '</dd></div><div><dt>' + esc(copy.weight) + '</dt><dd>' + esc(node.dataset.nodeWeight) + '</dd></div></dl>';
1395
1433
  }
1434
+ syncOrbits();
1396
1435
  }
1397
1436
  function selectHarness(id) {
1398
1437
  const node = nodeById('harness:' + id);
@@ -1410,7 +1449,9 @@ function renderScript(harnesses, copy) {
1410
1449
  nodes.forEach((node) => node.classList.toggle('is-hidden', mode === 'core' && node.dataset.core !== 'true'));
1411
1450
  const visibleIds = new Set(nodes.filter((node) => !node.classList.contains('is-hidden')).map((node) => node.dataset.nodeId));
1412
1451
  edges.forEach((edge) => edge.classList.toggle('is-hidden', mode === 'core' && (!visibleIds.has(edge.dataset.source) || !visibleIds.has(edge.dataset.target))));
1452
+ document.querySelectorAll('.orbit-system').forEach((system) => system.classList.toggle('is-mode-hidden', mode === 'core'));
1413
1453
  setStatus(mode === 'core' ? copy.coreMode : copy.showingAll);
1454
+ syncOrbits();
1414
1455
  }
1415
1456
  function filterRecommendation(value, button) {
1416
1457
  const alreadyActive = button.classList.contains('active-filter');
@@ -1424,6 +1465,7 @@ function renderScript(harnesses, copy) {
1424
1465
  cards.forEach((card) => card.classList.remove('is-filtered-out'));
1425
1466
  setStatus(copy.showingAll);
1426
1467
  setFilterStatus(copy.showingAll);
1468
+ syncOrbits();
1427
1469
  return;
1428
1470
  }
1429
1471
  button.classList.add('active-filter');
@@ -1438,6 +1480,7 @@ function renderScript(harnesses, copy) {
1438
1480
  const label = recLabelByFilter[value] || value;
1439
1481
  setStatus(copy.filteredTo + ': ' + label);
1440
1482
  setFilterStatus(copy.filteredTo + ': ' + label);
1483
+ syncOrbits();
1441
1484
  }
1442
1485
  interactiveNodes.forEach((node) => {
1443
1486
  node.addEventListener('click', () => selectNode(node));
@@ -1520,12 +1563,24 @@ function renderScript(harnesses, copy) {
1520
1563
  panState.y = event.clientY;
1521
1564
  applyView();
1522
1565
  });
1566
+ let suppressClick = false;
1523
1567
  const endPan = () => {
1568
+ suppressClick = Boolean(panState && panState.moved);
1524
1569
  panState = null;
1525
1570
  graphSvg.classList.remove('is-panning');
1526
1571
  };
1527
1572
  graphSvg.addEventListener('pointerup', endPan);
1528
1573
  graphSvg.addEventListener('pointercancel', endPan);
1574
+ graphSvg.addEventListener('click', (event) => {
1575
+ if (suppressClick) {
1576
+ suppressClick = false;
1577
+ return;
1578
+ }
1579
+ if (event.target.classList.contains('graph-bg') || event.target.closest('.starfield')) {
1580
+ clearSelection();
1581
+ if (selectedPanel && defaultSelectedPanel) selectedPanel.innerHTML = defaultSelectedPanel;
1582
+ }
1583
+ });
1529
1584
  graphSvg.addEventListener('dblclick', (event) => {
1530
1585
  event.preventDefault();
1531
1586
  resetView();
@@ -2193,6 +2248,70 @@ function renderStyles() {
2193
2248
  transition: opacity 160ms ease, transform 160ms ease;
2194
2249
  }
2195
2250
 
2251
+ .map-panel,
2252
+ .graph-canvas,
2253
+ .graph-canvas text {
2254
+ user-select: none;
2255
+ -webkit-user-select: none;
2256
+ -webkit-user-drag: none;
2257
+ }
2258
+
2259
+ .star-twinkle {
2260
+ animation: star-twinkle 3.4s ease-in-out var(--twinkle-delay, 0ms) infinite alternate;
2261
+ }
2262
+
2263
+ @keyframes star-twinkle {
2264
+ from { opacity: 0.08; }
2265
+ to { opacity: 0.6; }
2266
+ }
2267
+
2268
+ .orbit-ring {
2269
+ fill: none;
2270
+ stroke: var(--text-secondary);
2271
+ stroke-opacity: 0.14;
2272
+ stroke-width: 0.7;
2273
+ stroke-dasharray: 2 5;
2274
+ }
2275
+
2276
+ .orbit-spin {
2277
+ animation: orbit-rotate var(--orbit-dur, 40s) linear infinite;
2278
+ }
2279
+
2280
+ .orbit-spin.is-reverse { animation-direction: reverse; }
2281
+
2282
+ @keyframes orbit-rotate {
2283
+ to { transform: rotate(360deg); }
2284
+ }
2285
+
2286
+ .orbit-dot { opacity: 0.85; }
2287
+
2288
+ .orbit-system {
2289
+ transition: opacity 260ms ease;
2290
+ }
2291
+
2292
+ .orbit-system.is-hidden,
2293
+ .orbit-system.is-mode-hidden { opacity: 0; pointer-events: none; }
2294
+ .orbit-system.is-dimmed { opacity: 0.08; }
2295
+
2296
+ .planet-ring {
2297
+ fill: none;
2298
+ stroke: var(--text-secondary);
2299
+ stroke-opacity: 0.4;
2300
+ stroke-width: 1.4;
2301
+ pointer-events: none;
2302
+ }
2303
+
2304
+ .graph-canvas.has-selection .graph-node:not(.is-selected):not(.is-related) {
2305
+ opacity: 0.14;
2306
+ filter: blur(1.2px);
2307
+ }
2308
+
2309
+ .graph-canvas.has-selection .graph-edge:not(.is-related) { opacity: 0.05; }
2310
+
2311
+ .graph-canvas.has-selection .planet-ring { stroke-opacity: 0.1; }
2312
+
2313
+ .graph-canvas.has-selection .labels text { opacity: 0.18; transition: opacity 220ms ease; }
2314
+
2196
2315
  .map-controls-row {
2197
2316
  display: flex;
2198
2317
  gap: var(--space-2);
@@ -2300,6 +2419,8 @@ function renderStyles() {
2300
2419
  .graph-node,
2301
2420
  .graph-edge,
2302
2421
  .graph-canvas text,
2422
+ .orbit-spin,
2423
+ .star-twinkle,
2303
2424
  .page.is-active { animation: none; }
2304
2425
  .graph-node { transition: none; }
2305
2426
  }