tink-harness 1.9.5 → 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.5",
4
+ "version": "1.9.7",
5
5
  "author": {
6
6
  "name": "dotori"
7
7
  }
package/CHANGELOG.md CHANGED
@@ -6,6 +6,30 @@ 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
+
20
+ ## [1.9.6] - 2026-06-10
21
+
22
+ ### Added
23
+
24
+ - The harness map is now fully navigable: wheel zoom toward the cursor (0.4x–5x), drag to pan, +/− and reset controls, and double-click to return to the full view.
25
+ - Nodes render as 3D-style spheres with radial gradients, depth shadows on interactive nodes, and a vignette background for depth.
26
+ - Added a "How to read this map" card to the graph tab's right rail with plain-language explanations of circles, colors, lines, satellites, controls, and edge types.
27
+
28
+ ### Changed
29
+
30
+ - Reordered the graph right rail: reading guide → selected node → graph overview.
31
+ - Map help text now mentions zoom and drag controls.
32
+
9
33
  ## [1.9.5] - 2026-06-10
10
34
 
11
35
  ### Added
package/VERSIONING.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Versioning
2
2
 
3
- Current version: `1.9.5`
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.5",
3
+ "version": "1.9.7",
4
4
  "description": "Self-growing harnesses for Claude Code and Codex.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -48,7 +48,7 @@ const COPY = {
48
48
  heroText: 'Every visible Tink run, rule, memory reference, and harness relationship mapped into one local dashboard. This report only prepares suggestions and never edits reusable state.',
49
49
  generated: 'GENERATED',
50
50
  harnessMap: 'HARNESS MAP',
51
- mapHelp: 'Harnesses, rules, memory, and stages are mapped from visible Tink records. Click a node to inspect it.',
51
+ mapHelp: 'Harnesses, rules, memory, and stages are mapped from visible Tink records. Scroll to zoom, drag to move, click a node to inspect it.',
52
52
  graphControls: 'Graph controls',
53
53
  full: 'Full',
54
54
  core: 'Core',
@@ -146,6 +146,30 @@ const COPY = {
146
146
  runWindow: 'Run window',
147
147
  totalRuns: 'Runs',
148
148
  refCount: 'References',
149
+ zoomIn: 'Zoom in',
150
+ zoomOut: 'Zoom out',
151
+ zoomReset: 'Reset view',
152
+ mapGuideEyebrow: 'HOW TO READ',
153
+ mapGuideTitle: 'What is this map?',
154
+ mapGuideText: 'Each circle is something Tink knows about: a harness, a rule it loads, or a memory file it reads. The map is drawn only from visible local records.',
155
+ guideItems: [
156
+ ['Big circle', 'The more a harness is used, the bigger its circle.'],
157
+ ['Color', 'Blue circles are harnesses; gray ones are rules, memory, and stages.'],
158
+ ['Line', 'A line means "works together": a harness using a rule, reading memory, or leading to a next step.'],
159
+ ['Small dots', 'Tiny satellites around a harness are its usage, evidence, and score signals.']
160
+ ],
161
+ controlItems: [
162
+ ['Wheel / + −', 'Zoom in and out'],
163
+ ['Drag', 'Move around the map'],
164
+ ['Double-click', 'Back to full view'],
165
+ ['Click a circle', 'See its details below']
166
+ ],
167
+ relationTitle: 'What the lines mean',
168
+ relationItems: [
169
+ ['uses_rule', 'This harness loads that rule'],
170
+ ['uses_memory', 'This harness reads that memory file'],
171
+ ['sequence', 'This harness is usually followed by that step']
172
+ ],
149
173
  groups: [
150
174
  ['keep', 'Healthy harnesses', 'Ready to keep using'],
151
175
  ['weave', 'Weave candidates', 'Worth improving next'],
@@ -206,6 +230,30 @@ COPY.ko = {
206
230
  runWindow: '기록 기간',
207
231
  totalRuns: 'Run 수',
208
232
  refCount: '참조 횟수',
233
+ zoomIn: '확대',
234
+ zoomOut: '축소',
235
+ zoomReset: '전체 보기',
236
+ mapGuideEyebrow: '지도 읽는 법',
237
+ mapGuideTitle: '이 지도는 무엇인가요?',
238
+ mapGuideText: '원 하나하나가 Tink가 알고 있는 것입니다: 하네스, 하네스가 쓰는 규칙, 읽는 메모리 파일. 로컬에 보이는 기록만으로 그려집니다.',
239
+ guideItems: [
240
+ ['큰 원', '하네스를 많이 쓸수록 원이 커집니다.'],
241
+ ['색상', '파란 원이 하네스이고, 회색 계열은 규칙·메모리·단계입니다.'],
242
+ ['선', '"함께 일한다"는 뜻입니다. 하네스가 규칙을 쓰거나, 메모리를 읽거나, 다음 단계로 이어질 때 선이 생깁니다.'],
243
+ ['작은 점', '하네스 주변의 작은 점들은 사용·근거·점수 신호입니다.']
244
+ ],
245
+ controlItems: [
246
+ ['휠 / + −', '확대·축소'],
247
+ ['드래그', '지도 이동'],
248
+ ['더블클릭', '전체 보기로 복귀'],
249
+ ['원 클릭', '아래에서 상세 보기']
250
+ ],
251
+ relationTitle: '선의 의미',
252
+ relationItems: [
253
+ ['uses_rule', '이 하네스가 그 규칙을 불러옵니다'],
254
+ ['uses_memory', '이 하네스가 그 메모리 파일을 읽습니다'],
255
+ ['sequence', '이 하네스 다음에 그 단계가 자주 이어집니다']
256
+ ],
209
257
  navLabel: '탐색',
210
258
  operator: '작업자',
211
259
  online: 'Tink 온라인',
@@ -214,7 +262,7 @@ COPY.ko = {
214
262
  heroText: '보이는 Tink run, rule, memory reference, harness 관계를 하나의 로컬 대시보드로 보여줍니다. 이 보고서는 제안만 준비하며 재사용 상태를 직접 수정하지 않습니다.',
215
263
  generated: '생성 시각',
216
264
  harnessMap: '하네스 지도',
217
- mapHelp: '보이는 Tink 기록에서 하네스, rule, memory, stage 관계를 그립니다. 노드를 클릭하면 자세히 볼 수 있습니다.',
265
+ mapHelp: '보이는 Tink 기록에서 하네스, rule, memory, stage 관계를 그립니다. 휠로 확대, 드래그로 이동, 노드를 클릭하면 자세히 볼 수 있습니다.',
218
266
  graphControls: '그래프 조작',
219
267
  full: '전체',
220
268
  core: '핵심',
@@ -570,8 +618,7 @@ function buildGraphLayout(summary) {
570
618
  glow: score >= 50 || radius >= 20
571
619
  };
572
620
  });
573
- const augmented = [...positioned];
574
- const virtualEdges = [];
621
+ const orbitSystems = [];
575
622
  for (const node of positioned.filter((item) => item.type === 'harness')) {
576
623
  const harnessId = shortLabel(node.id);
577
624
  const harness = harnessById.get(harnessId);
@@ -579,38 +626,25 @@ function buildGraphLayout(summary) {
579
626
  const uses = Number(harness.signals?.uses || 0);
580
627
  const evidenceCount = (harness.evidence_handles || []).length;
581
628
  const factorCount = (harness.candidate_score?.factors || []).length;
582
- const satellites = [
583
- ...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) })),
584
- ...Array.from({ length: Math.min(6, evidenceCount) }, (_, index) => ({ kind: 'evidence', index, total: Math.min(6, evidenceCount), radius: 3.5 })),
585
- ...Array.from({ length: Math.min(5, factorCount) }, (_, index) => ({ kind: 'score', index, total: Math.min(5, factorCount), radius: 3.8 }))
586
- ];
587
- satellites.forEach((satellite, offset) => {
588
- const seed = hashString(`${node.id}:${satellite.kind}:${satellite.index}`);
589
- const angle = ((seed % 6283) / 1000) + offset * 0.45;
590
- const distance = node.radius + 24 + (seed % 42);
591
- const child = {
592
- id: `${satellite.kind}:${harnessId}:${satellite.index}`,
593
- type: satellite.kind,
594
- label: satellite.kind,
595
- weight: 1,
596
- x: clamp(node.x + Math.cos(angle) * distance, 25, 1065),
597
- y: clamp(node.y + Math.sin(angle) * distance, 25, 655),
598
- radius: satellite.radius,
599
- color: TYPE_COLORS[satellite.kind] || TYPE_COLORS.unknown,
600
- glow: satellite.kind === 'score'
601
- };
602
- augmented.push(child);
603
- virtualEdges.push({
604
- source: node.id,
605
- target: child.id,
606
- type: satellite.kind,
607
- count: 1
608
- });
609
- });
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
+ }
610
645
  }
611
- const byId = new Map(augmented.map((node) => [node.id, node]));
612
- const filteredEdges = getRenderableEdges(edges);
613
- const drawnEdges = [...filteredEdges, ...virtualEdges]
646
+ const byId = new Map(positioned.map((node) => [node.id, node]));
647
+ const drawnEdges = getRenderableEdges(edges)
614
648
  .map((edge) => ({
615
649
  ...edge,
616
650
  sourceNode: byId.get(edge.source),
@@ -619,15 +653,26 @@ function buildGraphLayout(summary) {
619
653
  .filter((edge) => edge.sourceNode && edge.targetNode)
620
654
  .slice(0, 240);
621
655
 
622
- return { nodes: augmented, edges: drawnEdges };
656
+ return { nodes: positioned, edges: drawnEdges, orbitSystems };
623
657
  }
624
658
 
625
659
  function renderGraphCanvas(summary, copy) {
626
- const { nodes, edges } = buildGraphLayout(summary);
660
+ const { nodes, edges, orbitSystems } = buildGraphLayout(summary);
627
661
  const strongest = nodes
628
662
  .filter((node) => node.type === 'harness')
629
663
  .sort((a, b) => Number(b.weight || 0) - Number(a.weight || 0))
630
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
+ });
631
676
  const mapTitle = copy.knowledgeGraph || copy.harnessMap || 'Harness map';
632
677
  const mapEyebrow = copy.harnessMap && copy.harnessMap !== mapTitle ? copy.harnessMap : '';
633
678
  return `
@@ -638,13 +683,40 @@ function renderGraphCanvas(summary, copy) {
638
683
  <h2 id="map-title">${escapeHtml(mapTitle)}</h2>
639
684
  <p>${escapeHtml(copy.mapHelp)}</p>
640
685
  </div>
641
- <div class="map-controls" aria-label="${escapeAttr(copy.graphControls)}">
642
- <button class="active" type="button" data-mode="full" aria-pressed="true">${escapeHtml(copy.full)}</button>
643
- <button type="button" data-mode="core" aria-pressed="false">${escapeHtml(copy.core)}</button>
686
+ <div class="map-controls-row">
687
+ <div class="map-controls" aria-label="${escapeAttr(copy.graphControls)}">
688
+ <button class="active" type="button" data-mode="full" aria-pressed="true">${escapeHtml(copy.full)}</button>
689
+ <button type="button" data-mode="core" aria-pressed="false">${escapeHtml(copy.core)}</button>
690
+ </div>
691
+ <div class="map-controls" aria-label="zoom">
692
+ <button type="button" data-zoom="in" aria-label="${escapeAttr(copy.zoomIn || 'Zoom in')}" title="${escapeAttr(copy.zoomIn || 'Zoom in')}">+</button>
693
+ <button type="button" data-zoom="out" aria-label="${escapeAttr(copy.zoomOut || 'Zoom out')}" title="${escapeAttr(copy.zoomOut || 'Zoom out')}">−</button>
694
+ <button type="button" data-zoom="reset" title="${escapeAttr(copy.zoomReset || 'Reset view')}">${escapeHtml(copy.zoomReset || 'Reset')}</button>
695
+ </div>
644
696
  </div>
645
697
  </div>
646
698
  <svg class="graph-canvas" viewBox="0 0 1090 680" role="img" aria-label="Harness health graph">
647
- <rect width="1090" height="680" fill="var(--bg-card)"/>
699
+ <defs>
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"/>
704
+ </radialGradient>
705
+ ${Object.entries(TYPE_COLORS).map(([type, color]) => `
706
+ <radialGradient id="node-grad-${escapeAttr(type)}" cx="32%" cy="28%" r="78%">
707
+ <stop offset="0%" style="stop-color: #FFFFFF; stop-opacity: 0.42"/>
708
+ <stop offset="38%" style="stop-color: ${escapeAttr(color)}; stop-opacity: 0.98"/>
709
+ <stop offset="100%" style="stop-color: ${escapeAttr(color)}; stop-opacity: 0.78"/>
710
+ </radialGradient>
711
+ `).join('')}
712
+ </defs>
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>
719
+ <g id="graph-viewport">
648
720
  <g class="edges">
649
721
  ${edges.map((edge, index) => `
650
722
  <line
@@ -662,6 +734,22 @@ function renderGraphCanvas(summary, copy) {
662
734
  />
663
735
  `).join('')}
664
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>
665
753
  <g class="nodes">
666
754
  ${nodes.map((node, index) => {
667
755
  const seed = hashString(node.id);
@@ -671,6 +759,9 @@ function renderGraphCanvas(summary, copy) {
671
759
  const floatY = (((seed >> 3) % 7) - 3).toFixed(1);
672
760
  return `
673
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
+ ` : ''}
674
765
  <circle
675
766
  class="graph-node ${node.type === 'harness' ? 'is-interactive' : ''}"
676
767
  style="--enter-delay: ${Math.min(index * 9, 1100)}ms"
@@ -686,8 +777,8 @@ function renderGraphCanvas(summary, copy) {
686
777
  cx="${node.x.toFixed(1)}"
687
778
  cy="${node.y.toFixed(1)}"
688
779
  r="${node.radius.toFixed(1)}"
689
- fill="${escapeAttr(node.color)}"
690
- fill-opacity="${node.type === 'harness' ? '0.96' : '0.82'}"
780
+ fill="url(#node-grad-${escapeAttr(TYPE_COLORS[node.type] ? node.type : 'unknown')})"
781
+ fill-opacity="${node.type === 'harness' ? '1' : '0.85'}"
691
782
  stroke="${escapeAttr('var(--text-secondary)')}"
692
783
  stroke-opacity="${node.glow ? '0.9' : '0.18'}"
693
784
  stroke-width="${node.glow ? '1.8' : '0.8'}"
@@ -703,6 +794,7 @@ function renderGraphCanvas(summary, copy) {
703
794
  <text x="${(node.x + node.radius + 7).toFixed(1)}" y="${(node.y + 4).toFixed(1)}">${escapeHtml(node.label)}</text>
704
795
  `).join('')}
705
796
  </g>
797
+ </g>
706
798
  </svg>
707
799
  <div class="graph-tooltip" id="graph-tooltip" role="status" aria-live="polite"></div>
708
800
  <div class="map-caption">
@@ -1008,6 +1100,39 @@ function renderGraphOverview(graph = {}, copy) {
1008
1100
  `;
1009
1101
  }
1010
1102
 
1103
+ function renderMapGuide(copy) {
1104
+ const guideItems = Array.isArray(copy.guideItems) ? copy.guideItems : [];
1105
+ const controlItems = Array.isArray(copy.controlItems) ? copy.controlItems : [];
1106
+ const relationItems = Array.isArray(copy.relationItems) ? copy.relationItems : [];
1107
+ return `
1108
+ <section class="insight-card map-guide">
1109
+ <div class="panel-title">
1110
+ <p class="eyebrow">${escapeHtml(copy.mapGuideEyebrow || 'HOW TO READ')}</p>
1111
+ <h2>${escapeHtml(copy.mapGuideTitle || 'What is this map?')}</h2>
1112
+ <p>${escapeHtml(copy.mapGuideText || '')}</p>
1113
+ </div>
1114
+ <ul class="guide-list">
1115
+ ${guideItems.map(([term, text]) => `
1116
+ <li><strong>${escapeHtml(term)}</strong><span>${escapeHtml(text)}</span></li>
1117
+ `).join('')}
1118
+ </ul>
1119
+ <ul class="guide-list guide-controls">
1120
+ ${controlItems.map(([key, text]) => `
1121
+ <li><kbd>${escapeHtml(key)}</kbd><span>${escapeHtml(text)}</span></li>
1122
+ `).join('')}
1123
+ </ul>
1124
+ ${relationItems.length ? `
1125
+ <p class="detail-label">${escapeHtml(copy.relationTitle || 'What the lines mean')}</p>
1126
+ <ul class="guide-list guide-relations">
1127
+ ${relationItems.map(([type, text]) => `
1128
+ <li><code>${escapeHtml(type)}</code><span>${escapeHtml(text)}</span></li>
1129
+ `).join('')}
1130
+ </ul>
1131
+ ` : ''}
1132
+ </section>
1133
+ `;
1134
+ }
1135
+
1011
1136
  function renderRoutingCard(copy) {
1012
1137
  const rules = Array.isArray(copy.routeRules) ? copy.routeRules : [];
1013
1138
  return `
@@ -1255,14 +1380,30 @@ function renderScript(harnesses, copy) {
1255
1380
  const setFilterStatus = (value) => {
1256
1381
  if (filterStatus) filterStatus.textContent = value;
1257
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
+ }
1258
1396
  function clearSelection() {
1259
1397
  nodes.forEach((item) => item.classList.remove('is-selected', 'is-related'));
1260
1398
  edges.forEach((item) => item.classList.remove('is-related'));
1261
1399
  cards.forEach((item) => item.classList.remove('is-selected'));
1400
+ if (graphCanvas) graphCanvas.classList.remove('has-selection');
1401
+ syncOrbits();
1262
1402
  }
1263
1403
  function selectNode(node) {
1264
1404
  clearSelection();
1265
1405
  node.classList.add('is-selected');
1406
+ if (graphCanvas) graphCanvas.classList.add('has-selection');
1266
1407
  const id = node.dataset.nodeId;
1267
1408
  const item = byHarnessId.get(id);
1268
1409
  edges.forEach((edge) => {
@@ -1290,6 +1431,7 @@ function renderScript(harnesses, copy) {
1290
1431
  } else if (selectedPanel) {
1291
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>';
1292
1433
  }
1434
+ syncOrbits();
1293
1435
  }
1294
1436
  function selectHarness(id) {
1295
1437
  const node = nodeById('harness:' + id);
@@ -1307,7 +1449,9 @@ function renderScript(harnesses, copy) {
1307
1449
  nodes.forEach((node) => node.classList.toggle('is-hidden', mode === 'core' && node.dataset.core !== 'true'));
1308
1450
  const visibleIds = new Set(nodes.filter((node) => !node.classList.contains('is-hidden')).map((node) => node.dataset.nodeId));
1309
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'));
1310
1453
  setStatus(mode === 'core' ? copy.coreMode : copy.showingAll);
1454
+ syncOrbits();
1311
1455
  }
1312
1456
  function filterRecommendation(value, button) {
1313
1457
  const alreadyActive = button.classList.contains('active-filter');
@@ -1321,6 +1465,7 @@ function renderScript(harnesses, copy) {
1321
1465
  cards.forEach((card) => card.classList.remove('is-filtered-out'));
1322
1466
  setStatus(copy.showingAll);
1323
1467
  setFilterStatus(copy.showingAll);
1468
+ syncOrbits();
1324
1469
  return;
1325
1470
  }
1326
1471
  button.classList.add('active-filter');
@@ -1335,6 +1480,7 @@ function renderScript(harnesses, copy) {
1335
1480
  const label = recLabelByFilter[value] || value;
1336
1481
  setStatus(copy.filteredTo + ': ' + label);
1337
1482
  setFilterStatus(copy.filteredTo + ': ' + label);
1483
+ syncOrbits();
1338
1484
  }
1339
1485
  interactiveNodes.forEach((node) => {
1340
1486
  node.addEventListener('click', () => selectNode(node));
@@ -1364,6 +1510,88 @@ function renderScript(harnesses, copy) {
1364
1510
  document.querySelectorAll('[data-mode]').forEach((button) => {
1365
1511
  button.addEventListener('click', () => applyMode(button.dataset.mode));
1366
1512
  });
1513
+ const graphSvg = document.querySelector('.graph-canvas');
1514
+ const graphViewport = document.getElementById('graph-viewport');
1515
+ if (graphSvg && graphViewport) {
1516
+ const view = { x: 0, y: 0, k: 1 };
1517
+ graphViewport.style.transformOrigin = '0 0';
1518
+ const applyView = () => {
1519
+ graphViewport.style.transform = 'translate(' + view.x + 'px, ' + view.y + 'px) scale(' + view.k + ')';
1520
+ };
1521
+ const svgPoint = (event) => {
1522
+ const pt = graphSvg.createSVGPoint();
1523
+ pt.x = event.clientX;
1524
+ pt.y = event.clientY;
1525
+ return pt.matrixTransform(graphSvg.getScreenCTM().inverse());
1526
+ };
1527
+ const zoomAt = (factor, cx, cy) => {
1528
+ const k = Math.min(5, Math.max(0.4, view.k * factor));
1529
+ const real = k / view.k;
1530
+ view.x = cx - (cx - view.x) * real;
1531
+ view.y = cy - (cy - view.y) * real;
1532
+ view.k = k;
1533
+ applyView();
1534
+ };
1535
+ const resetView = () => {
1536
+ graphViewport.classList.add('is-resetting');
1537
+ view.x = 0; view.y = 0; view.k = 1;
1538
+ applyView();
1539
+ setTimeout(() => graphViewport.classList.remove('is-resetting'), 360);
1540
+ };
1541
+ graphSvg.addEventListener('wheel', (event) => {
1542
+ event.preventDefault();
1543
+ const point = svgPoint(event);
1544
+ zoomAt(event.deltaY < 0 ? 1.15 : 1 / 1.15, point.x, point.y);
1545
+ }, { passive: false });
1546
+ let panState = null;
1547
+ graphSvg.addEventListener('pointerdown', (event) => {
1548
+ if (event.button !== 0) return;
1549
+ panState = { x: event.clientX, y: event.clientY, moved: false };
1550
+ graphSvg.setPointerCapture(event.pointerId);
1551
+ });
1552
+ graphSvg.addEventListener('pointermove', (event) => {
1553
+ if (!panState) return;
1554
+ const dx = event.clientX - panState.x;
1555
+ const dy = event.clientY - panState.y;
1556
+ if (!panState.moved && Math.abs(dx) + Math.abs(dy) < 3) return;
1557
+ panState.moved = true;
1558
+ graphSvg.classList.add('is-panning');
1559
+ const scale = 1090 / graphSvg.clientWidth;
1560
+ view.x += dx * scale;
1561
+ view.y += dy * scale;
1562
+ panState.x = event.clientX;
1563
+ panState.y = event.clientY;
1564
+ applyView();
1565
+ });
1566
+ let suppressClick = false;
1567
+ const endPan = () => {
1568
+ suppressClick = Boolean(panState && panState.moved);
1569
+ panState = null;
1570
+ graphSvg.classList.remove('is-panning');
1571
+ };
1572
+ graphSvg.addEventListener('pointerup', endPan);
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
+ });
1584
+ graphSvg.addEventListener('dblclick', (event) => {
1585
+ event.preventDefault();
1586
+ resetView();
1587
+ });
1588
+ document.querySelectorAll('[data-zoom]').forEach((button) => {
1589
+ button.addEventListener('click', () => {
1590
+ if (button.dataset.zoom === 'reset') return resetView();
1591
+ zoomAt(button.dataset.zoom === 'in' ? 1.3 : 1 / 1.3, 545, 340);
1592
+ });
1593
+ });
1594
+ }
1367
1595
  const VALID_TABS = ['home', 'harnesses', 'memory', 'graph', 'activity'];
1368
1596
  const navLinks = Array.from(document.querySelectorAll('.nav a[data-tab]'));
1369
1597
  const pages = Array.from(document.querySelectorAll('.page'));
@@ -2020,6 +2248,147 @@ function renderStyles() {
2020
2248
  transition: opacity 160ms ease, transform 160ms ease;
2021
2249
  }
2022
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
+
2315
+ .map-controls-row {
2316
+ display: flex;
2317
+ gap: var(--space-2);
2318
+ align-items: center;
2319
+ flex-wrap: wrap;
2320
+ }
2321
+
2322
+ .graph-canvas {
2323
+ cursor: grab;
2324
+ touch-action: none;
2325
+ }
2326
+
2327
+ .graph-canvas.is-panning { cursor: grabbing; }
2328
+
2329
+ #graph-viewport {
2330
+ will-change: transform;
2331
+ }
2332
+
2333
+ #graph-viewport.is-resetting {
2334
+ transition: transform 340ms cubic-bezier(0.2, 0.8, 0.2, 1);
2335
+ }
2336
+
2337
+ .graph-node.is-interactive {
2338
+ filter: drop-shadow(0 5px 7px rgba(0, 0, 0, 0.55));
2339
+ }
2340
+
2341
+ .graph-node.is-interactive:hover,
2342
+ .graph-node.is-interactive:focus-visible {
2343
+ filter: drop-shadow(0 9px 14px rgba(0, 0, 0, 0.65));
2344
+ }
2345
+
2346
+ .map-guide .panel-title p:last-child {
2347
+ font-size: 12px;
2348
+ line-height: 1.5;
2349
+ }
2350
+
2351
+ .guide-list {
2352
+ margin: var(--space-3) 0 0;
2353
+ padding: 0;
2354
+ list-style: none;
2355
+ display: grid;
2356
+ gap: var(--space-2);
2357
+ }
2358
+
2359
+ .guide-list li {
2360
+ display: grid;
2361
+ grid-template-columns: 76px 1fr;
2362
+ gap: var(--space-2);
2363
+ align-items: start;
2364
+ font-size: 12px;
2365
+ line-height: 1.45;
2366
+ color: var(--text-secondary);
2367
+ }
2368
+
2369
+ .guide-list strong {
2370
+ color: var(--text-primary);
2371
+ font-size: 12px;
2372
+ font-weight: 500;
2373
+ }
2374
+
2375
+ .guide-list kbd {
2376
+ font-family: var(--font-mono);
2377
+ font-size: 10px;
2378
+ color: var(--accent-text);
2379
+ border: 1px solid var(--border-default);
2380
+ border-radius: var(--radius-sm);
2381
+ background: var(--bg-hover);
2382
+ padding: 2px 5px;
2383
+ text-align: center;
2384
+ white-space: nowrap;
2385
+ }
2386
+
2387
+ .guide-controls { border-top: 1px solid var(--border-default); padding-top: var(--space-3); }
2388
+
2389
+ .guide-relations li { grid-template-columns: 96px 1fr; }
2390
+ .guide-relations code { font-size: 10px; }
2391
+
2023
2392
  .map-legend {
2024
2393
  display: flex;
2025
2394
  gap: var(--space-3);
@@ -2050,6 +2419,8 @@ function renderStyles() {
2050
2419
  .graph-node,
2051
2420
  .graph-edge,
2052
2421
  .graph-canvas text,
2422
+ .orbit-spin,
2423
+ .star-twinkle,
2053
2424
  .page.is-active { animation: none; }
2054
2425
  .graph-node { transition: none; }
2055
2426
  }
@@ -2850,9 +3221,10 @@ function renderReport(summary) {
2850
3221
  <aside class="right-rail" aria-label="Insights">
2851
3222
  <div data-rail="home harnesses memory activity">${renderStats(summary, copy)}</div>
2852
3223
  <div data-rail="home harnesses">${renderConfidence(summary, copy)}</div>
2853
- <div data-rail="graph">${renderGraphOverview(summary.graph || {}, copy)}</div>
2854
- <div data-rail="home harnesses">${renderImportantHarnesses(harnesses, copy)}</div>
3224
+ <div data-rail="graph">${renderMapGuide(copy)}</div>
2855
3225
  <div data-rail="graph">${renderSelectedPanel(harnesses, copy)}</div>
3226
+ <div data-rail="home harnesses">${renderImportantHarnesses(harnesses, copy)}</div>
3227
+ <div data-rail="graph">${renderGraphOverview(summary.graph || {}, copy)}</div>
2856
3228
  </aside>
2857
3229
  </div>
2858
3230
  ${renderContractMetadata(copy)}