tink-harness 1.9.4 → 1.9.5
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.
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,19 @@ All notable changes to Tink are tracked here.
|
|
|
6
6
|
|
|
7
7
|
No unreleased changes yet.
|
|
8
8
|
|
|
9
|
+
## [1.9.5] - 2026-06-10
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Run timeline entries now show a colored outcome badge, a monospace timestamp, and harness chips instead of a comma list.
|
|
14
|
+
- The Activity tab gained a summary strip: total runs, run window, and completed/blocked/failed/recorded counts.
|
|
15
|
+
- Memory cards now show reference counts and referencing-harness chips derived from `uses_memory` graph edges.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- The lifecycle generator now reads YAML-frontmatter `outcome:` values from run records, so completed runs are counted as successes instead of unknown.
|
|
20
|
+
- Removed a generic "blocked" word match that flagged runs as blocked when the word merely appeared in prose.
|
|
21
|
+
|
|
9
22
|
## [1.9.4] - 2026-06-10
|
|
10
23
|
|
|
11
24
|
### Added
|
package/VERSIONING.md
CHANGED
package/package.json
CHANGED
|
@@ -83,10 +83,13 @@ function extractRefs(text, refs) {
|
|
|
83
83
|
function parseRun(filePath, harnessIds, ruleRefs, memoryRefs) {
|
|
84
84
|
const text = fs.readFileSync(filePath, 'utf8');
|
|
85
85
|
const statusMatch = text.match(/^Status:\s*([A-Za-z_-]+)/mi);
|
|
86
|
-
const
|
|
86
|
+
const outcomeMatch = text.match(/^outcome:\s*"?([A-Za-z_-]+)"?/mi);
|
|
87
|
+
const status = statusMatch
|
|
88
|
+
? statusMatch[1].toLowerCase()
|
|
89
|
+
: (outcomeMatch ? outcomeMatch[1].toLowerCase() : 'unknown');
|
|
87
90
|
const harnesses = extractSelectedHarnesses(text, harnessIds);
|
|
88
91
|
const failed = /check_failed|failed check|required check failed|verification failed/i.test(text);
|
|
89
|
-
const blocked = status === 'blocked' || /check_blocked
|
|
92
|
+
const blocked = status === 'blocked' || /check_blocked/i.test(text);
|
|
90
93
|
const completed = status === 'completed' || status === 'pass' || /npm test passed|verification passed/i.test(text);
|
|
91
94
|
return {
|
|
92
95
|
path: filePath,
|
|
@@ -143,6 +143,9 @@ const COPY = {
|
|
|
143
143
|
historyHelp: 'Approved reusable-state changes from the maintenance ledger, newest first.',
|
|
144
144
|
historyEmpty: 'No ledger history yet.',
|
|
145
145
|
sortNote: 'Sorted by usage',
|
|
146
|
+
runWindow: 'Run window',
|
|
147
|
+
totalRuns: 'Runs',
|
|
148
|
+
refCount: 'References',
|
|
146
149
|
groups: [
|
|
147
150
|
['keep', 'Healthy harnesses', 'Ready to keep using'],
|
|
148
151
|
['weave', 'Weave candidates', 'Worth improving next'],
|
|
@@ -200,6 +203,9 @@ COPY.ko = {
|
|
|
200
203
|
historyHelp: '유지보수 장부에 기록된 승인 변경 이력을 최신순으로 보여줍니다.',
|
|
201
204
|
historyEmpty: '아직 장부 기록이 없습니다.',
|
|
202
205
|
sortNote: '사용량 순 정렬',
|
|
206
|
+
runWindow: '기록 기간',
|
|
207
|
+
totalRuns: 'Run 수',
|
|
208
|
+
refCount: '참조 횟수',
|
|
203
209
|
navLabel: '탐색',
|
|
204
210
|
operator: '작업자',
|
|
205
211
|
online: 'Tink 온라인',
|
|
@@ -767,15 +773,26 @@ function dedupeTimelineEvents(events = [], harnessIds = null, limit = 8) {
|
|
|
767
773
|
}
|
|
768
774
|
|
|
769
775
|
function renderTimelineItems(items, copy) {
|
|
770
|
-
return items.map((event) =>
|
|
776
|
+
return items.map((event) => {
|
|
777
|
+
const outcome = timelineOutcomeClass(event);
|
|
778
|
+
const chips = (event.harnesses || []).slice(0, 6);
|
|
779
|
+
return `
|
|
771
780
|
<li>
|
|
772
|
-
<span class="dot ${escapeHtml(
|
|
781
|
+
<span class="dot ${escapeHtml(outcome)}"></span>
|
|
773
782
|
<div>
|
|
774
|
-
<
|
|
775
|
-
|
|
783
|
+
<div class="run-row">
|
|
784
|
+
<span class="run-badge ${escapeHtml(outcome)}">${escapeHtml(timelineOutcomeLabel(event, copy))}</span>
|
|
785
|
+
<time>${escapeHtml(shortDate(event.date))}</time>
|
|
786
|
+
</div>
|
|
787
|
+
<div class="run-chips">
|
|
788
|
+
${chips.length
|
|
789
|
+
? chips.map((id) => `<span class="co-chip">${escapeHtml(id)}</span>`).join('')
|
|
790
|
+
: `<span class="run-empty">${escapeHtml(copy.noHarnessRecorded)}</span>`}
|
|
791
|
+
</div>
|
|
776
792
|
</div>
|
|
777
793
|
</li>
|
|
778
|
-
|
|
794
|
+
`;
|
|
795
|
+
}).join('') || `<li><span class="dot observe"></span><div><strong>${escapeHtml(copy.noRunEvents)}</strong><p>${escapeHtml(copy.runRecordsWillAppear)}</p></div></li>`;
|
|
779
796
|
}
|
|
780
797
|
|
|
781
798
|
function renderTimeline(events = [], copy, harnessIds = null, options = {}) {
|
|
@@ -1070,19 +1087,26 @@ function renderHomePage(summary, copy, harnesses, harnessIds) {
|
|
|
1070
1087
|
function renderMemoryPage(summary, copy) {
|
|
1071
1088
|
const harnesses = getVisibleHarnesses(Array.isArray(summary.harnesses) ? summary.harnesses : []);
|
|
1072
1089
|
const refs = new Map();
|
|
1090
|
+
const ensureRef = (key) => {
|
|
1091
|
+
if (!refs.has(key)) refs.set(key, { users: new Set(), count: 0 });
|
|
1092
|
+
return refs.get(key);
|
|
1093
|
+
};
|
|
1073
1094
|
for (const harness of harnesses) {
|
|
1074
1095
|
for (const ref of harness.signals?.memory_refs || []) {
|
|
1075
|
-
|
|
1076
|
-
if (!refs.has(key)) refs.set(key, new Set());
|
|
1077
|
-
refs.get(key).add(harness.id);
|
|
1096
|
+
ensureRef(normalizePath(ref)).users.add(harness.id);
|
|
1078
1097
|
}
|
|
1079
1098
|
}
|
|
1099
|
+
for (const edge of getRenderableEdges(summary.graph?.edges || [])) {
|
|
1100
|
+
if (edge.type !== 'uses_memory') continue;
|
|
1101
|
+
const entry = ensureRef(normalizePath(String(edge.target).replace(/^memory:/, '')));
|
|
1102
|
+
entry.users.add(shortLabel(edge.source));
|
|
1103
|
+
entry.count += Number(edge.count || 1);
|
|
1104
|
+
}
|
|
1080
1105
|
for (const node of getRenderableNodes(summary.graph?.nodes || [])) {
|
|
1081
1106
|
if (node.type !== 'memory') continue;
|
|
1082
|
-
|
|
1083
|
-
if (!refs.has(key)) refs.set(key, new Set());
|
|
1107
|
+
ensureRef(normalizePath(String(node.id).replace(/^memory:/, '')));
|
|
1084
1108
|
}
|
|
1085
|
-
const entries = [...refs.entries()].sort(([a], [b]) => a.
|
|
1109
|
+
const entries = [...refs.entries()].sort(([, a], [, b]) => b.count - a.count || b.users.size - a.users.size);
|
|
1086
1110
|
return `
|
|
1087
1111
|
<section class="page-head">
|
|
1088
1112
|
<p class="eyebrow">${escapeHtml(copy.memoryEyebrow || 'MEMORY')}</p>
|
|
@@ -1091,15 +1115,21 @@ function renderMemoryPage(summary, copy) {
|
|
|
1091
1115
|
</section>
|
|
1092
1116
|
${entries.length ? `
|
|
1093
1117
|
<div class="memory-grid">
|
|
1094
|
-
${entries.map(([file,
|
|
1118
|
+
${entries.map(([file, info]) => `
|
|
1095
1119
|
<article class="insight-card memory-card">
|
|
1096
1120
|
<h3><code>${escapeHtml(file)}</code></h3>
|
|
1097
1121
|
<dl>
|
|
1098
1122
|
<div>
|
|
1099
|
-
<dt>${escapeHtml(copy.
|
|
1100
|
-
<dd>${
|
|
1123
|
+
<dt>${escapeHtml(copy.refCount || 'References')}</dt>
|
|
1124
|
+
<dd>${escapeHtml(formatNumber(Math.max(info.count, info.users.size)))}</dd>
|
|
1101
1125
|
</div>
|
|
1102
1126
|
</dl>
|
|
1127
|
+
<p class="detail-label">${escapeHtml(copy.referencedBy || 'Referenced by')}</p>
|
|
1128
|
+
<div class="co-used-chips">
|
|
1129
|
+
${[...info.users].length
|
|
1130
|
+
? [...info.users].sort().map((id) => `<span class="co-chip">${escapeHtml(id)}</span>`).join('')
|
|
1131
|
+
: `<span class="run-empty">${escapeHtml(copy.none || 'None')}</span>`}
|
|
1132
|
+
</div>
|
|
1103
1133
|
</article>
|
|
1104
1134
|
`).join('')}
|
|
1105
1135
|
</div>
|
|
@@ -1109,12 +1139,35 @@ function renderMemoryPage(summary, copy) {
|
|
|
1109
1139
|
|
|
1110
1140
|
function renderActivityPage(summary, copy, harnessIds) {
|
|
1111
1141
|
const items = dedupeTimelineEvents(summary.timeline || [], harnessIds, 30);
|
|
1142
|
+
const all = Array.isArray(summary.timeline) ? summary.timeline : [];
|
|
1143
|
+
const counts = { success: 0, blocked: 0, failed: 0, recorded: 0 };
|
|
1144
|
+
for (const event of all) {
|
|
1145
|
+
const key = timelineOutcomeClass(event);
|
|
1146
|
+
counts[key in counts ? key : 'recorded'] += 1;
|
|
1147
|
+
}
|
|
1148
|
+
const window = summary.run_window || {};
|
|
1149
|
+
const windowText = window.from && window.to
|
|
1150
|
+
? `${shortDate(window.from)} ~ ${shortDate(window.to)}`
|
|
1151
|
+
: renderCopyValue('', copy);
|
|
1152
|
+
const summaryCells = [
|
|
1153
|
+
[copy.totalRuns || 'Runs', formatNumber(window.run_count || all.length)],
|
|
1154
|
+
[copy.runWindow || 'Run window', windowText],
|
|
1155
|
+
[copy.timelineCompleted || 'Completed', formatNumber(counts.success)],
|
|
1156
|
+
[copy.timelineBlocked || 'Blocked', formatNumber(counts.blocked)],
|
|
1157
|
+
[copy.timelineFailed || 'Failed', formatNumber(counts.failed)],
|
|
1158
|
+
[copy.timelineRecorded || 'Recorded', formatNumber(counts.recorded)]
|
|
1159
|
+
];
|
|
1112
1160
|
return `
|
|
1113
1161
|
<section class="page-head">
|
|
1114
1162
|
<p class="eyebrow">${escapeHtml(copy.activityEyebrow || 'ACTIVITY')}</p>
|
|
1115
1163
|
<h1>${escapeHtml(copy.activityTitle || 'Run activity')}</h1>
|
|
1116
1164
|
<p>${escapeHtml(copy.activityHelp || '')}</p>
|
|
1117
1165
|
</section>
|
|
1166
|
+
<section class="activity-summary">
|
|
1167
|
+
${summaryCells.map(([label, value]) => `
|
|
1168
|
+
<article><span>${escapeHtml(label)}</span><strong>${escapeHtml(value)}</strong></article>
|
|
1169
|
+
`).join('')}
|
|
1170
|
+
</section>
|
|
1118
1171
|
<section class="timeline activity-feed">
|
|
1119
1172
|
<ol>
|
|
1120
1173
|
${renderTimelineItems(items, copy)}
|
|
@@ -2603,6 +2656,79 @@ function renderStyles() {
|
|
|
2603
2656
|
.activity-feed { padding: var(--space-4); }
|
|
2604
2657
|
.activity-feed ol { gap: var(--space-3); }
|
|
2605
2658
|
|
|
2659
|
+
.run-row {
|
|
2660
|
+
display: flex;
|
|
2661
|
+
align-items: center;
|
|
2662
|
+
gap: var(--space-2);
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
.run-row time {
|
|
2666
|
+
color: var(--text-secondary);
|
|
2667
|
+
font-size: 11px;
|
|
2668
|
+
font-family: var(--font-mono);
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
.run-badge {
|
|
2672
|
+
display: inline-block;
|
|
2673
|
+
font-size: 10px;
|
|
2674
|
+
font-weight: 500;
|
|
2675
|
+
text-transform: uppercase;
|
|
2676
|
+
letter-spacing: 0.05em;
|
|
2677
|
+
border: 1px solid var(--border-default);
|
|
2678
|
+
border-radius: var(--radius-sm);
|
|
2679
|
+
padding: 2px 6px;
|
|
2680
|
+
color: var(--text-secondary);
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
.run-badge.success { color: var(--success); border-color: var(--success-dim); background: var(--success-dim); }
|
|
2684
|
+
.run-badge.blocked { color: var(--warning); border-color: var(--warning-dim); background: var(--warning-dim); }
|
|
2685
|
+
.run-badge.failed { color: var(--danger); border-color: var(--danger-dim); background: var(--danger-dim); }
|
|
2686
|
+
.run-badge.recorded { background: var(--bg-hover); }
|
|
2687
|
+
|
|
2688
|
+
.run-chips {
|
|
2689
|
+
display: flex;
|
|
2690
|
+
flex-wrap: wrap;
|
|
2691
|
+
gap: 4px;
|
|
2692
|
+
margin-top: 5px;
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
.run-empty {
|
|
2696
|
+
color: var(--text-muted);
|
|
2697
|
+
font-size: 11px;
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
.activity-summary {
|
|
2701
|
+
display: grid;
|
|
2702
|
+
grid-template-columns: repeat(6, minmax(0, 1fr));
|
|
2703
|
+
gap: var(--space-2);
|
|
2704
|
+
margin-bottom: var(--space-3);
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
.activity-summary article {
|
|
2708
|
+
border: 1px solid var(--border-default);
|
|
2709
|
+
border-radius: var(--radius-lg);
|
|
2710
|
+
background: var(--bg-card);
|
|
2711
|
+
padding: var(--space-3);
|
|
2712
|
+
}
|
|
2713
|
+
|
|
2714
|
+
.activity-summary span {
|
|
2715
|
+
display: block;
|
|
2716
|
+
color: var(--text-secondary);
|
|
2717
|
+
font-size: 11px;
|
|
2718
|
+
letter-spacing: 0.06em;
|
|
2719
|
+
text-transform: uppercase;
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
.activity-summary strong {
|
|
2723
|
+
display: block;
|
|
2724
|
+
margin-top: var(--space-1);
|
|
2725
|
+
font-size: 16px;
|
|
2726
|
+
font-family: var(--font-mono);
|
|
2727
|
+
font-weight: 600;
|
|
2728
|
+
letter-spacing: -0.02em;
|
|
2729
|
+
overflow-wrap: anywhere;
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2606
2732
|
@media (max-width: 1180px) {
|
|
2607
2733
|
.app-shell { grid-template-columns: 200px minmax(0, 1fr); }
|
|
2608
2734
|
.right-rail {
|
|
@@ -2618,6 +2744,7 @@ function renderStyles() {
|
|
|
2618
2744
|
.stats-grid { grid-template-columns: 1fr; }
|
|
2619
2745
|
.home-stats { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
2620
2746
|
.home-columns { grid-template-columns: 1fr; }
|
|
2747
|
+
.activity-summary { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
2621
2748
|
}
|
|
2622
2749
|
|
|
2623
2750
|
@media (max-width: 760px) {
|
|
@@ -2639,6 +2766,8 @@ function renderStyles() {
|
|
|
2639
2766
|
.stats-grid { grid-template-columns: 1fr; }
|
|
2640
2767
|
.home-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
2641
2768
|
.memory-grid { grid-template-columns: 1fr; }
|
|
2769
|
+
.activity-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
2770
|
+
.harness-grid { grid-template-columns: 1fr; }
|
|
2642
2771
|
.map-head { display: block; }
|
|
2643
2772
|
.map-controls { margin-top: var(--space-2); width: max-content; }
|
|
2644
2773
|
.right-rail { padding: var(--space-4); }
|