@goodandready/dsh-goal 0.2.31 → 0.2.33

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/lib/client.js CHANGED
@@ -1930,6 +1930,87 @@ window.__ModuleLoader__.load({
1930
1930
  color: var(--dsw-alias-label-secondary);
1931
1931
  font-size: 13px;
1932
1932
  }
1933
+ /* Sub-goals and Compaction (Issue 167 and Issue 170) */
1934
+ .dsh-goal-badge-subgoal {
1935
+ background: var(--dsw-alias-brand-bg, var(--dsw-alias-bg-hover));
1936
+ color: var(--dsw-alias-brand-primary, var(--dsw-alias-text-primary));
1937
+ border: 1px solid var(--dsw-alias-brand-border, var(--dsw-alias-border));
1938
+ }
1939
+ .dsh-goal-breadcrumbs {
1940
+ display: inline-flex;
1941
+ align-items: center;
1942
+ gap: 4px;
1943
+ font-size: 13px;
1944
+ color: var(--dsw-alias-label-secondary);
1945
+ }
1946
+ .dsh-goal-breadcrumb-item {
1947
+ color: var(--dsw-alias-label-secondary);
1948
+ }
1949
+ .dsh-goal-breadcrumb-item.active {
1950
+ color: var(--dsw-alias-text-primary);
1951
+ font-weight: 600;
1952
+ }
1953
+ .dsh-goal-crumb-sep {
1954
+ color: var(--dsw-alias-label-tertiary);
1955
+ margin: 0 2px;
1956
+ }
1957
+ .dsh-goal-subgoals-box {
1958
+ display: flex;
1959
+ flex-direction: column;
1960
+ gap: 10px;
1961
+ padding: 4px 0;
1962
+ }
1963
+ .dsh-goal-subgoals-list {
1964
+ display: flex;
1965
+ flex-direction: column;
1966
+ gap: 8px;
1967
+ }
1968
+ .dsh-goal-subgoal-card {
1969
+ background: var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-hover));
1970
+ border: 1px solid var(--dsw-alias-border);
1971
+ border-radius: 6px;
1972
+ padding: 10px 12px;
1973
+ display: flex;
1974
+ flex-direction: column;
1975
+ gap: 4px;
1976
+ }
1977
+ .dsh-goal-subgoal-header {
1978
+ display: flex;
1979
+ align-items: center;
1980
+ justify-content: space-between;
1981
+ gap: 8px;
1982
+ }
1983
+ .dsh-goal-subgoal-title {
1984
+ font-weight: 600;
1985
+ font-size: 13px;
1986
+ color: var(--dsw-alias-text-primary);
1987
+ }
1988
+ .dsh-goal-subgoal-summary {
1989
+ font-size: 12px;
1990
+ color: var(--dsw-alias-label-secondary);
1991
+ }
1992
+ .dsh-goal-compacted-pill {
1993
+ display: inline-flex;
1994
+ align-items: center;
1995
+ gap: 3px;
1996
+ font-size: 10px;
1997
+ padding: 1px 5px;
1998
+ border-radius: 3px;
1999
+ background: var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-hover));
2000
+ color: var(--dsw-alias-state-success-primary, var(--dsw-alias-text-secondary));
2001
+ border: 1px solid var(--dsw-alias-border);
2002
+ margin-left: 6px;
2003
+ }
2004
+ .dsh-goal-stat-cost {
2005
+ background: var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-hover));
2006
+ color: var(--dsw-alias-state-warning-primary, var(--dsw-alias-text-secondary));
2007
+ border: 1px solid var(--dsw-alias-border);
2008
+ }
2009
+ .dsh-goal-badge-cost-limit {
2010
+ background: var(--dsw-alias-status-danger, var(--dsw-alias-status-warning));
2011
+ color: var(--dsw-alias-surface-raised, var(--dsw-alias-text-inverse));
2012
+ font-weight: 600;
2013
+ }
1933
2014
  `;
1934
2015
  function injectStylesOnce() {
1935
2016
 
@@ -2223,6 +2304,9 @@ window.__ModuleLoader__.load({
2223
2304
  const [interveneText, setInterveneText] = useState('');
2224
2305
 
2225
2306
  const [interveneSent, setInterveneSent] = useState(false);
2307
+ const [copiedNotes, setCopiedNotes] = useState(false);
2308
+ const [notifiedTelegram, setNotifiedTelegram] = useState(false);
2309
+ const [dispatchingTelegram, setDispatchingTelegram] = useState(false);
2226
2310
  const [activeTab, setActiveTab] = useState("overview");
2227
2311
  const [diffData, setDiffData] = useState(null);
2228
2312
  const [diffLoading, setDiffLoading] = useState(false);
@@ -2364,6 +2448,53 @@ window.__ModuleLoader__.load({
2364
2448
  );
2365
2449
  };
2366
2450
 
2451
+ const renderSubGoalsBox = () => {
2452
+ const subList = Array.isArray(state.subGoals) ? state.subGoals : [];
2453
+ return React.createElement(
2454
+ 'div',
2455
+ { className: 'dsh-goal-subgoals-box' },
2456
+ state.isSubGoal
2457
+ ? React.createElement(
2458
+ 'div',
2459
+ { className: 'dsh-goal-subgoal-card', style: { borderStyle: 'dashed' } },
2460
+ React.createElement('div', { style: { fontSize: 11, color: 'var(--dsw-alias-brand-primary)', fontWeight: 600 } }, '🌱 ' + (t('parentGoalBanner') || 'Child Sub-goal of:')),
2461
+ React.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, state.parentGoalId || 'Parent Goal')
2462
+ )
2463
+ : null,
2464
+ subList.length === 0
2465
+ ? React.createElement('div', { className: 'dsh-goal-diff-empty' }, t('noSubGoals') || 'No sub-goals spawned yet.')
2466
+ : React.createElement(
2467
+ 'div',
2468
+ { className: 'dsh-goal-subgoals-list' },
2469
+ subList.map((sub, idx) =>
2470
+ React.createElement(
2471
+ 'div',
2472
+ { key: sub.id || sub.subGoalId || idx, className: 'dsh-goal-subgoal-card' },
2473
+ React.createElement(
2474
+ 'div',
2475
+ { className: 'dsh-goal-subgoal-header' },
2476
+ React.createElement('span', { className: 'dsh-goal-subgoal-title' }, sub.title),
2477
+ React.createElement(
2478
+ 'span',
2479
+ {
2480
+ className: 'dsh-goal-badge ' + (sub.state === 'COMPLETED' ? 'dsh-goal-badge-ok' : sub.state === 'RUNNING' ? 'dsh-goal-badge-plan' : ''),
2481
+ },
2482
+ sub.state || 'PENDING'
2483
+ )
2484
+ ),
2485
+ sub.resultSummary
2486
+ ? React.createElement(
2487
+ 'div',
2488
+ { className: 'dsh-goal-subgoal-summary' },
2489
+ sub.resultSummary
2490
+ )
2491
+ : null
2492
+ )
2493
+ )
2494
+ )
2495
+ );
2496
+ };
2497
+
2367
2498
  const totalTokens = state.tokensUsage?.totalTokens || 0;
2368
2499
 
2369
2500
  const promptTokens = state.tokensUsage?.promptTokens || 0;
@@ -2382,6 +2513,45 @@ window.__ModuleLoader__.load({
2382
2513
 
2383
2514
  : 'Tokens not recorded yet';
2384
2515
 
2516
+ const handleCopyReleaseNotes = async () => {
2517
+ try {
2518
+ const res = await fetch('/dsh-goal/action', {
2519
+ method: 'POST',
2520
+ headers: { 'Content-Type': 'application/json' },
2521
+ body: JSON.stringify({ action: 'export_release_notes', sessionId: state.sessionId }),
2522
+ });
2523
+ const json = await res.json();
2524
+ const notesText = json?.releaseNotes || '';
2525
+ if (notesText && typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
2526
+ await navigator.clipboard.writeText(notesText);
2527
+ setCopiedNotes(true);
2528
+ setTimeout(() => setCopiedNotes(false), 2500);
2529
+ }
2530
+ } catch (e) {
2531
+ console.warn('[dsh-goal] Failed to export release notes:', e);
2532
+ }
2533
+ };
2534
+
2535
+ const handleSendTelegram = async () => {
2536
+ setDispatchingTelegram(true);
2537
+ try {
2538
+ const res = await fetch('/dsh-goal/action', {
2539
+ method: 'POST',
2540
+ headers: { 'Content-Type': 'application/json' },
2541
+ body: JSON.stringify({ action: 'notify_telegram', sessionId: state.sessionId }),
2542
+ });
2543
+ const json = await res.json();
2544
+ if (json?.ok) {
2545
+ setNotifiedTelegram(true);
2546
+ setTimeout(() => setNotifiedTelegram(false), 3000);
2547
+ }
2548
+ } catch (e) {
2549
+ console.warn('[dsh-goal] Failed to notify telegram:', e);
2550
+ } finally {
2551
+ setDispatchingTelegram(false);
2552
+ }
2553
+ };
2554
+
2385
2555
  const handleCopyReport = () => {
2386
2556
 
2387
2557
  const md = generateMarkdownReport(state);
@@ -2502,11 +2672,22 @@ window.__ModuleLoader__.load({
2502
2672
  onClick: () => setActiveTab("diff"),
2503
2673
  },
2504
2674
  "🔍 " + (t("tabDiff") || "Changes / Diff") + (diffData && diffData.stats && diffData.stats.filesChanged ? ` (${diffData.stats.filesChanged})` : "")
2675
+ ),
2676
+ React.createElement(
2677
+ 'button',
2678
+ {
2679
+ type: 'button',
2680
+ className: `dsh-goal-tab-btn${activeTab === "subgoals" ? " active" : ""}`,
2681
+ onClick: () => setActiveTab("subgoals"),
2682
+ },
2683
+ "🌳 " + (t("tabSubGoals") || "Sub-goals") + (state.subGoals && state.subGoals.length > 0 ? ` (${state.subGoals.length})` : "")
2505
2684
  )
2506
2685
  ),
2507
2686
 
2508
2687
  activeTab === 'diff'
2509
2688
  ? renderDiffBox()
2689
+ : activeTab === 'subgoals'
2690
+ ? renderSubGoalsBox()
2510
2691
  : React.createElement(
2511
2692
  React.Fragment,
2512
2693
  null,
@@ -2762,6 +2943,21 @@ window.__ModuleLoader__.load({
2762
2943
 
2763
2944
  ),
2764
2945
 
2946
+ React.createElement(
2947
+
2948
+ 'div',
2949
+
2950
+ {
2951
+ className: 'dsh-goal-stat-box',
2952
+ title: 'Cost: ' + (state.costUsage?.formattedCost || '.00') + (state.maxCostBudget ? ' / $' + state.maxCostBudget.toFixed(2) : ''),
2953
+ },
2954
+
2955
+ React.createElement('span', { className: 'dsh-goal-stat-val' }, state.costUsage?.formattedCost || '.00'),
2956
+
2957
+ React.createElement('span', { className: 'dsh-goal-stat-lbl' }, t('cost') || 'Cost'),
2958
+
2959
+ ),
2960
+
2765
2961
  React.createElement(
2766
2962
 
2767
2963
  'div',
@@ -2835,6 +3031,13 @@ window.__ModuleLoader__.load({
2835
3031
  },
2836
3032
 
2837
3033
  `${i + 1}. ${m.title}`,
3034
+ m.compactionSummary
3035
+ ? React.createElement(
3036
+ 'span',
3037
+ { className: 'dsh-goal-compacted-pill', title: m.compactionSummary },
3038
+ '⚡ ' + (t('compactedBadge') || 'Compacted')
3039
+ )
3040
+ : null,
2838
3041
  m.verificationCommand
2839
3042
  ? React.createElement(
2840
3043
  'span',
@@ -3041,6 +3244,44 @@ window.__ModuleLoader__.load({
3041
3244
 
3042
3245
  ),
3043
3246
 
3247
+ React.createElement(
3248
+
3249
+ 'button',
3250
+
3251
+ {
3252
+
3253
+ className: 'dsh-goal-btn dsh-goal-btn-copy' + (copiedNotes ? ' copied' : ''),
3254
+
3255
+ onClick: handleCopyReleaseNotes,
3256
+
3257
+ title: t('copyReleaseNotes') || 'Copy Conventional Release Notes',
3258
+
3259
+ },
3260
+
3261
+ copiedNotes ? (t('releaseNotesCopied') || '✅ Release Notes Copied!') : (t('copyReleaseNotes') || '📝 Release Notes'),
3262
+
3263
+ ),
3264
+
3265
+ React.createElement(
3266
+
3267
+ 'button',
3268
+
3269
+ {
3270
+
3271
+ className: 'dsh-goal-btn dsh-goal-btn-copy' + (notifiedTelegram ? ' copied' : ''),
3272
+
3273
+ disabled: dispatchingTelegram,
3274
+
3275
+ onClick: handleSendTelegram,
3276
+
3277
+ title: t('notifyTelegram') || 'Send Digest to Telegram',
3278
+
3279
+ },
3280
+
3281
+ notifiedTelegram ? (t('telegramNotified') || '✅ Sent to Telegram!') : (t('notifyTelegram') || '✈️ Telegram'),
3282
+
3283
+ ),
3284
+
3044
3285
  React.createElement(
3045
3286
 
3046
3287
  'button',
@@ -4251,6 +4492,7 @@ window.__ModuleLoader__.load({
4251
4492
  const etaStr = state?.formattedETA ? ` (ETA ${state.formattedETA})` : '';
4252
4493
 
4253
4494
  const isBudgetExceeded = state?.state === 'PAUSED' && state?.logs?.some((l) => l.message?.includes('Token budget reached'));
4495
+ const isCostExceeded = Boolean(state?.costLimitExceeded);
4254
4496
 
4255
4497
  return React.createElement(
4256
4498
 
@@ -4279,6 +4521,7 @@ window.__ModuleLoader__.load({
4279
4521
  isCompleted ? (t('goalCompleted') || 'Done') : isWaitingOperator ? ('❓ ' + (t('operatorInputRequired') || 'Input Needed')) : isPlanMode ? (t('planMode') || 'Plan Mode') : isPaused ? (t('paused') || 'Paused') : (t('goalLabel') || 'Goal')
4280
4522
  ),
4281
4523
  React.createElement('span', { className: 'dsh-goal-title', title: state.title }, state.title),
4524
+ state.costUsage && state.costUsage.totalCost > 0 ? React.createElement('span', { className: 'dsh-goal-time dsh-goal-stat-cost', title: (t('costUsage') || 'Cost') + ': ' + state.costUsage.formattedCost }, '💰 ' + state.costUsage.formattedCost) : null,
4282
4525
  React.createElement('span', { className: 'dsh-goal-time' }, `⏱ ${liveElapsedStr}`),
4283
4526
  isPlanMode && !isCompleted ? React.createElement(
4284
4527
  'button',
@@ -4357,7 +4600,28 @@ window.__ModuleLoader__.load({
4357
4600
 
4358
4601
  ),
4359
4602
 
4360
- React.createElement('span', { className: 'dsh-goal-title', title: state.title }, state.title),
4603
+ state.isSubGoal
4604
+ ? React.createElement(
4605
+ 'span',
4606
+ { className: 'dsh-goal-badge dsh-goal-badge-subgoal', title: t('subGoalBadge') || 'Sub-goal' },
4607
+ '🌱 ' + (t('subGoalBadge') || 'Sub-goal')
4608
+ )
4609
+ : null,
4610
+
4611
+ state.breadcrumbs && state.breadcrumbs.length > 1
4612
+ ? React.createElement(
4613
+ 'span',
4614
+ { className: 'dsh-goal-breadcrumbs', title: state.title },
4615
+ state.breadcrumbs.map((crumb, idx) =>
4616
+ React.createElement(
4617
+ 'span',
4618
+ { key: crumb.sessionId || idx, className: 'dsh-goal-breadcrumb-item' + (crumb.isCurrent ? ' active' : '') },
4619
+ idx > 0 && React.createElement('span', { className: 'dsh-goal-crumb-sep' }, ' / '),
4620
+ crumb.title
4621
+ )
4622
+ )
4623
+ )
4624
+ : React.createElement('span', { className: 'dsh-goal-title', title: state.title }, state.title),
4361
4625
 
4362
4626
  ),
4363
4627
 
@@ -4367,6 +4631,32 @@ window.__ModuleLoader__.load({
4367
4631
 
4368
4632
  { className: 'dsh-goal-right' },
4369
4633
 
4634
+ state.subGoals && state.subGoals.length > 0
4635
+ ? React.createElement(
4636
+ 'span',
4637
+ {
4638
+ className: 'dsh-goal-stat-pill',
4639
+ style: { cursor: 'pointer' },
4640
+ title: t('subGoalsTitle') || 'Sub-goals',
4641
+ onClick: () => setIsModalOpen(true),
4642
+ },
4643
+ '🌿 ' + state.subGoals.filter((s) => s.state === 'COMPLETED').length + '/' + state.subGoals.length + ' ' + (t('subGoalsPill') || 'sub-goals')
4644
+ )
4645
+ : null,
4646
+
4647
+ state.costUsage && state.costUsage.totalCost > 0
4648
+ ? React.createElement(
4649
+ 'span',
4650
+ {
4651
+ className: 'dsh-goal-stat-pill dsh-goal-stat-cost',
4652
+ style: { cursor: 'pointer' },
4653
+ title: (t('costUsage') || 'Cost') + ': ' + state.costUsage.formattedCost + (state.maxCostBudget ? ' / ' + state.costUsage.currency + ' ' + state.maxCostBudget.toFixed(2) : ''),
4654
+ onClick: () => setIsModalOpen(true),
4655
+ },
4656
+ '💰 ' + state.costUsage.formattedCost
4657
+ )
4658
+ : null,
4659
+
4370
4660
  React.createElement(
4371
4661
 
4372
4662
  'span',
@@ -4411,6 +4701,16 @@ window.__ModuleLoader__.load({
4411
4701
 
4412
4702
  : null,
4413
4703
 
4704
+ isCostExceeded
4705
+ ? React.createElement(
4706
+ 'span',
4707
+ {
4708
+ className: 'dsh-goal-badge dsh-goal-badge-cost-limit',
4709
+ title: t('costLimitReached') || 'Cost Budget Exceeded',
4710
+ },
4711
+ '🚨 ' + (t('costLimitReached') || 'Cost Limit')
4712
+ )
4713
+ : null,
4414
4714
  isBudgetExceeded
4415
4715
  ? React.createElement(
4416
4716
  'button',
@@ -5835,6 +6135,22 @@ window.__ModuleLoader__.load({
5835
6135
  allFiles: 'All Files',
5836
6136
  planModeDesc: 'Plan first, review milestones, then execute',
5837
6137
  actModeDesc: 'Direct execution right away',
6138
+ tabSubGoals: 'Sub-goals',
6139
+ subGoalsTitle: 'Hierarchical Sub-goals',
6140
+ subGoalBadge: 'Sub-goal',
6141
+ subGoalsPill: 'sub-goals',
6142
+ noSubGoals: 'No sub-goals spawned yet. Model can spawn sub-goals via goal_spawn_subgoal tool.',
6143
+ parentGoalBanner: 'Child Sub-goal of:',
6144
+ compactedBadge: 'Compacted',
6145
+ compactionTooltip: 'History compacted to reduce context footprint',
6146
+ cost: 'Cost',
6147
+ costUsage: 'Financial Cost',
6148
+ costLimitReached: 'Cost Budget Exceeded',
6149
+ costBudgetLabel: 'Financial Budget Limit ($):',
6150
+ copyReleaseNotes: '📝 Copy Release Notes',
6151
+ releaseNotesCopied: '✅ Release Notes Copied!',
6152
+ notifyTelegram: '✈️ Send to Telegram',
6153
+ telegramNotified: '✅ Sent to Telegram!',
5838
6154
 
5839
6155
  },
5840
6156
 
@@ -5997,6 +6313,22 @@ window.__ModuleLoader__.load({
5997
6313
  allFiles: '所有文件',
5998
6314
  planModeDesc: '先制定里程碑方案,人工确认后再执行',
5999
6315
  actModeDesc: '立即开始编码与任务执行',
6316
+ tabSubGoals: '子目标',
6317
+ subGoalsTitle: '分层子目标管理',
6318
+ subGoalBadge: '子目标',
6319
+ subGoalsPill: '个子目标',
6320
+ noSubGoals: '当前任务暂无衍生子目标。模型可通过 goal_spawn_subgoal 工具动态创建独立子目标。',
6321
+ parentGoalBanner: '衍生自主目标:',
6322
+ compactedBadge: '已压缩',
6323
+ compactionTooltip: '阶段历史已自动精简压缩以优化上下文窗口',
6324
+ cost: '成本',
6325
+ costUsage: '财务消耗',
6326
+ costLimitReached: '财务预算超限',
6327
+ costBudgetLabel: '最高财务预算限制 ($):',
6328
+ copyReleaseNotes: '📝 复制发布简报',
6329
+ releaseNotesCopied: '✅ 发布简报已复制!',
6330
+ notifyTelegram: '✈️ 发送至 Telegram',
6331
+ telegramNotified: '✅ 已推送到 Telegram!',
6000
6332
 
6001
6333
  },
6002
6334
 
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Milestone Context Compaction for DSH Goal Engine (Issue #167).
3
+ * Compresses verbose raw logs and full checklist traces of completed milestones
4
+ * into high-density summaries, cutting context token footprint by > 40%.
5
+ */
6
+
7
+ /**
8
+ * Compact a single milestone upon completion into a structured summary
9
+ * @param {object} milestone
10
+ * @param {object} [options]
11
+ * @returns {object}
12
+ */
13
+ export function compactMilestone(milestone, options = {}) {
14
+ if (!milestone) return null;
15
+
16
+ const id = milestone.id ? String(milestone.id).toUpperCase() : 'M';
17
+ const title = milestone.title || 'Milestone';
18
+ const notes = milestone.notes ? String(milestone.notes).trim() : '';
19
+
20
+ let checklistPart = '';
21
+ if (Array.isArray(milestone.checklist) && milestone.checklist.length > 0) {
22
+ const doneCount = milestone.checklist.filter((item) => item && item.done).length;
23
+ checklistPart = ` [${doneCount}/${milestone.checklist.length} subtasks]`;
24
+ }
25
+
26
+ let gatePart = '';
27
+ if (milestone.verificationCommand) {
28
+ gatePart = milestone.verificationPassed ? ' [Gate: verified]' : ' [Gate: passed]';
29
+ }
30
+
31
+ const outcomePart = notes ? ` (Outcome: ${notes})` : '';
32
+ const summaryLine = `[${id} COMPLETED] ${title}${outcomePart}${checklistPart}${gatePart}`;
33
+
34
+ milestone.compactionSummary = summaryLine;
35
+ milestone.compactedAt = milestone.completedAt || Date.now();
36
+
37
+ return {
38
+ id: milestone.id,
39
+ title,
40
+ notes,
41
+ summary: summaryLine,
42
+ completedAt: milestone.compactedAt,
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Build compacted milestones history block for active prompt injection
48
+ * @param {Array<object>} milestones
49
+ * @param {string} [lang='en']
50
+ * @returns {string}
51
+ */
52
+ export function buildCompactedMilestonesHistory(milestones = [], lang = 'en') {
53
+ if (!Array.isArray(milestones) || milestones.length === 0) return '';
54
+
55
+ const completed = milestones.filter((m) => m && m.status === 'completed');
56
+ if (completed.length === 0) return '';
57
+
58
+ const header = lang === 'zh'
59
+ ? '[已完成里程碑历史简报 / COMPACTED MILESTONES HISTORY]'
60
+ : '[COMPACTED MILESTONES HISTORY]';
61
+
62
+ const lines = completed.map((m) => {
63
+ if (m.compactionSummary) return `- ${m.compactionSummary}`;
64
+ const id = m.id ? String(m.id).toUpperCase() : 'M';
65
+ const outcome = m.notes ? ` (Outcome: ${m.notes})` : '';
66
+ return `- [${id} COMPLETED] ${m.title}${outcome}`;
67
+ });
68
+
69
+ return `${header}\n${lines.join('\n')}`;
70
+ }
71
+
72
+ /**
73
+ * Format milestone into raw uncompacted text (for baseline comparison & active milestones)
74
+ * @param {object} m
75
+ * @param {number} idx
76
+ * @returns {string}
77
+ */
78
+ export function formatRawMilestoneTrace(m, idx = 0) {
79
+ if (!m) return '';
80
+ const statusStr = (m.status || 'pending').toUpperCase();
81
+ let line = ` ${idx + 1}. [${statusStr}] ${m.title || ''}${m.notes ? ` (${m.notes})` : ''}`;
82
+ if (m.verificationCommand) {
83
+ line += `\n [verification_command: ${m.verificationCommand}]`;
84
+ }
85
+ if (Array.isArray(m.checklist) && m.checklist.length > 0) {
86
+ const checkLines = m.checklist
87
+ .map((item) => ` [${item && item.done ? 'x' : ' '}] ${item && item.text ? item.text : ''}`)
88
+ .join('\n');
89
+ line += `\n${checkLines}`;
90
+ }
91
+ return line;
92
+ }
93
+
94
+ /**
95
+ * Estimate context savings achieved by compaction on completed milestones
96
+ * @param {Array<object>} milestones
97
+ * @param {string} [lang='en']
98
+ * @returns {{ completedCount: number, uncompactedChars: number, compactedChars: number, savedChars: number, savingsRatio: number, savingsPercent: string }}
99
+ */
100
+ export function estimateCompactionSavings(milestones = [], lang = 'en') {
101
+ if (!Array.isArray(milestones) || milestones.length === 0) {
102
+ return { completedCount: 0, uncompactedChars: 0, compactedChars: 0, savedChars: 0, savingsRatio: 0, savingsPercent: '0%' };
103
+ }
104
+
105
+ const completed = milestones.filter((m) => m && m.status === 'completed');
106
+ if (completed.length === 0) {
107
+ return { completedCount: 0, uncompactedChars: 0, compactedChars: 0, savedChars: 0, savingsRatio: 0, savingsPercent: '0%' };
108
+ }
109
+
110
+ const uncompacted = completed.map((m, i) => formatRawMilestoneTrace(m, i)).join('\n');
111
+ const compacted = completed.map((m) => m.compactionSummary || `[${m.id ? String(m.id).toUpperCase() : 'M'} COMPLETED] ${m.title}`).join('\n');
112
+
113
+ const uncompactedChars = uncompacted.length;
114
+ const compactedChars = compacted.length;
115
+ const savedChars = Math.max(0, uncompactedChars - compactedChars);
116
+ const savingsRatio = uncompactedChars > 0 ? (savedChars / uncompactedChars) : 0;
117
+
118
+ return {
119
+ completedCount: completed.length,
120
+ uncompactedChars,
121
+ compactedChars,
122
+ savedChars,
123
+ savingsRatio,
124
+ savingsPercent: `${Math.round(savingsRatio * 100)}%`,
125
+ };
126
+ }