@starwish-ai/xingyuan-dsh 0.5.9 → 0.6.0

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
@@ -5,6 +5,51 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.6.0] - 2026-09-05
9
+
10
+ > **Note:** behavioral revision — long-term ratios (wish progress, progress charts) join
11
+ > the commitment semantics: unclaimed tasks no longer count in the denominator, and
12
+ > achievement additionally requires zero unclaimed tasks. No data format change; upgrades
13
+ > from 0.5.x are seamless — the switch can only raise progress, and every newly-qualifying
14
+ > wish carries unclaimed candidates that the achievement gate holds back (nothing archives
15
+ > by accident).
16
+
17
+ ### Added
18
+ - **Achievement gate and the "wrapping up" state**: a wish achieves only at 100% progress
19
+ with no unclaimed tasks (single formula `wishProgressFromAgg` shared by the write path and
20
+ the read side). Full progress with unclaimed tasks parks the wish at "wrapping up" — the
21
+ bar is full, the wish is not archived — and the card shows the pending count with the
22
+ guidance line "claim to continue, or delete to achieve". The call is always the user's:
23
+ the assistant never claims or deletes on its own to force achievement.
24
+ - **Gate flips always reach the model**: check-in / claim / update / cancel / delete / create
25
+ (including batches) compare before/after wish views and, on achieve / wrap-up / rollback,
26
+ say so in the tool reply and re-emit the wish event, so chat card, reply and wishes page
27
+ stay in sync. Count drift without a flip also re-emits (the "N to claim" badge never
28
+ freezes).
29
+ - **Single-source display bits**: `pendingCount` / `settled` / `planning` / `achieved` are
30
+ produced by `freshWishes` and delivered via `/api/wishes` and `xingyuan/wish` events;
31
+ replays of older events degrade honestly to a plain progress display.
32
+ - **Wording standard**: all user-facing and verbatim-relay surfaces (i18n, tool replies,
33
+ prompt guides, standalone pages) say "待领取的任务" / "待收尾" — no internal jargon.
34
+ Locked by a banned-word test (`test/prompts.test.ts`) and a shared guidance-line constant.
35
+
36
+ ### Changed
37
+ - `wishProgress` and `taskCompletionRate` now divide by claimed-task days only; the
38
+ subtitle is fixed to "已领取任务" (the conditional "含未领取任务" wording is gone —
39
+ one metric, one meaning).
40
+ - Growth-page achievement counts and the `wishAchievement` chart consume the derived
41
+ achieved bit instead of `progress >= 100`, matching the wish card exactly.
42
+ - The session-opening overview reads fresh derived views (not the stored `archived` bit)
43
+ and distinguishes "all wishes achieved" from "no wishes yet".
44
+
45
+ ### Fixed
46
+ - Creating a bare wish showed "进度 0%" on the chat card while the wishes page said
47
+ "计划中" (the create event dropped the derived bits).
48
+ - Claiming a standalone task (no parent wish) claimed its days were folded into "wish
49
+ progress".
50
+ - Batch-delete replies glued the achievement guidance onto the summary line; batch paths
51
+ no longer re-scan the task table per wish on the after side.
52
+
8
53
  ## [0.5.9] - 2026-09-04
9
54
 
10
55
  > **Note:** this release migrates the plugin to DeepSeek Harness `0.1.2-rc.1`
package/README.en-US.md CHANGED
@@ -12,6 +12,7 @@ XingYuan is a wish & habit-building companion, shipped as a DeepSeek Harness plu
12
12
 
13
13
  - **Conversation-driven**: create wishes / tasks in natural language with duplicate detection
14
14
  - **Opportunity-day check-ins**: check-in, make-up, cancel, and future pre-checks; progress always recomputed against today
15
+ - **Commitment-based progress**: only claimed tasks count; a wish achieves when everything committed is done and nothing is left to claim — full progress with unclaimed tasks leaves it "wrapping up" for your call (claim to continue, or delete to achieve), never nagging, never deciding for you
15
16
  - **Micro-actions**: break an overwhelming goal into 3–7 tiny steps and walk through them guided
16
17
  - **Growth system**: levels Lv.1–Lv.10, streak bonuses, stat cards, 30-day bar chart
17
18
  - **Session view tabs**: Today / Wishes / Tasks / Calendar / Growth / Memory — buttons call action endpoints directly; shown only in XingYuan-preset sessions by default, switchable to always show/hide and per-tab in Settings
package/README.md CHANGED
@@ -12,6 +12,8 @@
12
12
 
13
13
  - **对话驱动**:自然语言创建愿望 / 任务,自动查重相似项
14
14
  - **机会日打卡**:周期任务打卡、补卡、取消、未来预勾,进度跨天恒为最新
15
+ - **承诺口径进度**:只有领取的任务算进度;达成 = 承诺全部完成且没有待领取的任务——
16
+ 做完但有没领的,愿望停在「待收尾」等你决定(领了继续,或删掉就达成),不催也不代劳
15
17
  - **微行动拆解**:把无从下手的目标拆成 3–7 个小步,引导式逐个完成或跳过
16
18
  - **成长体系**:等级 Lv.1–Lv.10、连续加成、统计卡与近 30 天柱状图
17
19
  - **会话视图页**:今日 / 愿望 / 任务 / 日历 / 成长 / 记忆 六个标签页,页面按钮直连动作接口;
@@ -22,9 +22,11 @@ function WishView(props) {
22
22
  style: categoryVars(wish.wish.colorKey, wish.wish.categoryName),
23
23
  }, t('badge.wish')), createElement('span', { className: 'xy-title' }, deleted ? t('state.deleted', { title: wish.wish.title }) : wish.wish.title)),
24
24
  // 已删除卡不再渲染活体进度(删除态 × 进度百分比自相矛盾)
25
+ // 三态位(achieved/planning/settled/pendingCount)来自事件载荷(wishProgressFromAgg 派生),
26
+ // 与愿望页/工具回包同一称呼;旧事件无这些位则如实降级为纯「进度 X%」显示(不重建谓词,§5.2 规则 7)
25
27
  createElement('div', { className: 'xy-meta' }, deleted
26
28
  ? wish.wish.categoryName
27
- : `${wish.wish.categoryName} · ${t('wish.progress', { percent: wish.wish.progress })}`));
29
+ : `${wish.wish.categoryName} · ${wish.wish.achieved ? t('wish.achieved') : wish.wish.planning ? t('wish.planning') : t('wish.progress', { percent: wish.wish.progress })}${wish.wish.pendingCount ? ` · ${t('wish.pendingCount', { n: wish.wish.pendingCount })}` : ''}${wish.wish.settled ? ` · ${t('wish.settled')}` : ''}`));
28
30
  }
29
31
  function TaskView(props) {
30
32
  const t = useXyT();
@@ -81,12 +81,11 @@ export const CHART_SUBTITLE_KEYS = {
81
81
  '按任务 TopN': 'chart.subtitle.byTaskTopN',
82
82
  '按愿望 TopN': 'chart.subtitle.byWishTopN',
83
83
  '按实际打卡时刻分桶;过滤范围为打卡日区间': 'chart.subtitle.byHourBucket',
84
- '%(应打天数完成率)': 'chart.subtitle.progressPercent',
85
- '%(应打天数完成率,含未领取任务)': 'chart.subtitle.progressPercentPending',
84
+ '%(已领取任务完成率)': 'chart.subtitle.claimedPercent',
86
85
  };
87
86
  /**
88
87
  * 副标题本地化:固定短语查表;含数字的模板(近 N 天 / A/B 天 / 当前 A 天 / 最长 B 天 /
89
- * 含未领取任务)按正则重排——服务端模板变化时未知副标题原样回显(诚实降级)。
88
+ * 已领取任务口径)按正则重排——服务端模板变化时未知副标题原样回显(诚实降级)。
90
89
  */
91
90
  export function localizeChartSubtitle(subtitle) {
92
91
  const fixed = CHART_SUBTITLE_KEYS[subtitle];
@@ -95,9 +94,9 @@ export function localizeChartSubtitle(subtitle) {
95
94
  let m = /^近 (\d+) 天$/.exec(subtitle);
96
95
  if (m !== null)
97
96
  return t('chart.subtitle.lastDays', { n: m[1] });
98
- m = /^(\d+)\/(\d+) 天(含未领取任务)$/.exec(subtitle);
97
+ m = /^(\d+)\/(\d+) 天(已领取任务)$/.exec(subtitle);
99
98
  if (m !== null)
100
- return t('chart.subtitle.daysInclPending', { done: m[1], due: m[2] });
99
+ return t('chart.subtitle.daysClaimed', { done: m[1], due: m[2] });
101
100
  m = /^(\d+)\/(\d+) 天$/.exec(subtitle);
102
101
  if (m !== null)
103
102
  return t('chart.subtitle.daysRatio', { done: m[1], due: m[2] });
@@ -58,11 +58,18 @@ export const ZH_DICT = {
58
58
  'state.done': '已完成',
59
59
  // --- 愿望卡 / 愿望页 ---
60
60
  'wish.progress': '进度 {percent}%',
61
- 'wish.progressHasPending': '含未领取任务',
61
+ 'wish.planning': '计划中',
62
+ 'wish.achieved': '已达成 🎉',
63
+ 'wish.pendingCount': '{n} 个任务待领取',
64
+ 'wish.settled': '待收尾',
65
+ 'wish.settleHint': '答应自己的都做完了——待领取的领了继续,或删掉就达成',
62
66
  'wish.eta': '预计完成 {date}',
63
67
  'wish.noTasks': '暂无下属任务——在对话里告诉我,让我为它推荐打卡任务',
64
68
  'wish.pageTitle': '我的愿望',
65
- 'wish.summary': '{active} 个进行中{achieved}', 'wish.achievedSuffix': ' · {n} 个已达成',
69
+ 'wish.summary': '{active} 个进行中{planning}{settled}{achieved}',
70
+ 'wish.planningSuffix': ' · {n} 个计划中',
71
+ 'wish.settledSuffix': ' · {n} 个待收尾',
72
+ 'wish.achievedSuffix': ' · {n} 个已达成',
66
73
  'wish.sectionAchieved': '已达成',
67
74
  'wish.empty.title': '还没有愿望',
68
75
  'wish.empty.hint': '告诉我想实现什么,我来帮你拆解成可坚持的计划。',
@@ -343,10 +350,9 @@ export const ZH_DICT = {
343
350
  'chart.subtitle.byTaskTopN': '按任务 TopN',
344
351
  'chart.subtitle.byWishTopN': '按愿望 TopN',
345
352
  'chart.subtitle.byHourBucket': '按实际打卡时刻分桶;过滤范围为打卡日区间',
346
- 'chart.subtitle.progressPercent': '%(应打天数完成率)',
347
- 'chart.subtitle.progressPercentPending': '%(应打天数完成率,含未领取任务)',
353
+ 'chart.subtitle.claimedPercent': '%(已领取任务完成率)',
348
354
  'chart.subtitle.lastDays': '近 {n} 天',
349
- 'chart.subtitle.daysInclPending': '{done}/{due} 天(含未领取任务)',
355
+ 'chart.subtitle.daysClaimed': '{done}/{due} 天(已领取任务)',
350
356
  'chart.subtitle.daysRatio': '{done}/{due} 天',
351
357
  'chart.subtitle.streak': '当前 {current} 天 / 最长 {best} 天',
352
358
  // --- 详情聚合视图 ---
@@ -480,11 +486,17 @@ const EN_DICT = {
480
486
  'state.deleted': 'Deleted “{title}”',
481
487
  'state.done': 'Done',
482
488
  'wish.progress': 'Progress {percent}%',
483
- 'wish.progressHasPending': 'includes unclaimed tasks',
489
+ 'wish.planning': 'Planning',
490
+ 'wish.achieved': 'Achieved 🎉',
491
+ 'wish.pendingCount': '{n} to claim',
492
+ 'wish.settled': 'Wrapping up',
493
+ 'wish.settleHint': 'Everything you committed to is done — claim the rest to keep going, or delete them to achieve the wish',
484
494
  'wish.eta': 'Target date {date}',
485
495
  'wish.noTasks': 'No tasks yet — tell me in chat and I will suggest check-in tasks for it',
486
496
  'wish.pageTitle': 'My Wishes',
487
- 'wish.summary': '{active} in progress{achieved}',
497
+ 'wish.summary': '{active} in progress{planning}{settled}{achieved}',
498
+ 'wish.planningSuffix': ' · {n} planning',
499
+ 'wish.settledSuffix': ' · {n} wrapping up',
488
500
  'wish.achievedSuffix': ' · {n} achieved',
489
501
  'wish.sectionAchieved': 'Achieved',
490
502
  'wish.empty.title': 'No wishes yet',
@@ -751,10 +763,9 @@ const EN_DICT = {
751
763
  'chart.subtitle.byTaskTopN': 'By task (top N)',
752
764
  'chart.subtitle.byWishTopN': 'By wish (top N)',
753
765
  'chart.subtitle.byHourBucket': 'Bucketed by actual check-in time; filtered to the check-in date range',
754
- 'chart.subtitle.progressPercent': '% of scheduled-day completion',
755
- 'chart.subtitle.progressPercentPending': '% of scheduled-day completion (incl. unclaimed)',
766
+ 'chart.subtitle.claimedPercent': '% of committed-task completion',
756
767
  'chart.subtitle.lastDays': 'Last {n} days',
757
- 'chart.subtitle.daysInclPending': '{done}/{due} days (incl. unclaimed)',
768
+ 'chart.subtitle.daysClaimed': '{done}/{due} days (claimed tasks)',
758
769
  'chart.subtitle.daysRatio': '{done}/{due} days',
759
770
  'chart.subtitle.streak': 'Current {current}d / best {best}d',
760
771
  'detail.grid.title': 'Check-in record',
@@ -39,8 +39,12 @@ export function WishesPage() {
39
39
  const data = page.data;
40
40
  if (data === undefined)
41
41
  return createElement(PageSkeleton);
42
- const active = data.wishes.filter((w) => !w.archived);
42
+ // 页头四态计数:进行中/计划中/待收尾/已达成各计各的,与卡头徽章同一称呼
43
+ // (消灭「摘要算进行中、卡上写计划中」的并见歧义);全部取服务端派生位,展示层不重建谓词
43
44
  const achieved = data.wishes.filter((w) => w.archived);
45
+ const settledCount = data.wishes.filter((w) => w.settled).length;
46
+ const planningCount = data.wishes.filter((w) => !w.archived && w.planning).length;
47
+ const active = data.wishes.filter((w) => !w.archived);
44
48
  /** 删除愿望(级联下属任务与打卡记录,服务端同一写路径):确认后直连动作并整页刷新。
45
49
  * 行移除后触发按钮销毁、焦点落空——刷新完成后把焦点交给页面标题兜底。 */
46
50
  const removeWish = (wish) => {
@@ -70,37 +74,45 @@ export function WishesPage() {
70
74
  ? [line, createElement(TaskDetailPanel, { key: `d-${task.taskId}`, id: `xy-task-detail-${task.taskId}`, taskId: task.taskId, today: data.today, onChanged: page.reload })]
71
75
  : [line];
72
76
  });
73
- const card = (wish) => createElement('div', { key: wish.wishId, className: 'xy-wishcard' }, createElement('div', { className: 'xy-card-head' }, createElement('span', {
74
- className: 'xy-badge xy-badge-cat',
75
- style: categoryVars(wish.colorKey, wish.categoryName),
76
- }, wish.categoryName), createElement('span', { className: 'xy-title' }, wish.title),
77
- // 长期尺度 = 计划口径(分母含未领取任务的应打天数,§5.2 规则 7):愿望含未领取
78
- // 任务时必须呈现「含未领取任务」标注,避免把计划分母误读为承诺分母
79
- createElement('span', { className: 'xy-progress-num' }, t('wish.progress', { percent: wish.progress }), wish.tasks.some((task) => task.status === 'pending')
80
- ? createElement('span', { className: 'xy-meta' }, ` · ${t('wish.progressHasPending')}`)
81
- : null), createElement('button', {
82
- className: 'xy-btn xy-btn-danger xy-btn-icon',
83
- disabled: deleting,
84
- 'aria-label': t('common.delete') + ' · ' + wish.title,
85
- title: t('common.delete'),
86
- onClick: () => removeWish(wish),
87
- }, createElement(IconTrash))), wish.description !== undefined ? createElement('div', { className: 'xy-meta' }, wish.description) : null, wish.estimatedCompletionDate !== undefined
88
- ? createElement('div', { className: 'xy-meta' }, t('wish.eta', { date: formatMediumDate(wish.estimatedCompletionDate) }))
89
- : null,
90
- // 与今日页 hero 进度条同一语义语法:读屏可感知进度(role+valuenow),不只是一根哑条
91
- createElement('div', {
92
- className: 'xy-bar',
93
- role: 'progressbar',
94
- 'aria-valuemin': 0,
95
- 'aria-valuemax': 100,
96
- 'aria-valuenow': wish.progress,
97
- 'aria-label': t('wish.progress', { percent: wish.progress })
98
- + (wish.tasks.some((task) => task.status === 'pending') ? ` · ${t('wish.progressHasPending')}` : ''),
99
- }, createElement('div', { className: 'xy-bar-fill', style: { transform: `scaleX(${Math.min(wish.progress, 100) / 100})` } })), wish.tasks.length > 0
100
- ? createElement('div', { className: 'xy-wishtasks' }, ...wishTasks(wish))
101
- : createElement('div', { className: 'xy-meta' }, t('wish.noTasks')));
77
+ const card = (wish) => {
78
+ // 承诺口径(§5.2 规则 7):进度分母只计已领取任务。展示三态:
79
+ // 计划中(无已领取任务)/ 进行中(正常百分比)/ 待结算(满进度而有候选——不归档,附收尾指引)。
80
+ // settled/pendingCount/planning 直取服务端 wishProgressFromAgg 派生位(随 /api/wishes 下发),
81
+ // 展示层禁止重建 `progress>=100 && 有候选` 闸门或 planning 谓词(单一判定源,防与工具回包分叉)
82
+ const { pendingCount, settled, planning } = wish;
83
+ const progressLabel = planning ? t('wish.planning') : t('wish.progress', { percent: wish.progress });
84
+ const pendingMeta = pendingCount > 0 ? ` · ${t('wish.pendingCount', { n: pendingCount })}` : '';
85
+ return createElement('div', { key: wish.wishId, className: 'xy-wishcard' }, createElement('div', { className: 'xy-card-head' }, createElement('span', {
86
+ className: 'xy-badge xy-badge-cat',
87
+ style: categoryVars(wish.colorKey, wish.categoryName),
88
+ }, wish.categoryName), createElement('span', { className: 'xy-title' }, wish.title), createElement('span', { className: 'xy-progress-num' }, progressLabel, pendingCount > 0
89
+ ? createElement('span', { className: 'xy-meta' }, pendingMeta)
90
+ : null), createElement('button', {
91
+ className: 'xy-btn xy-btn-danger xy-btn-icon',
92
+ disabled: deleting,
93
+ 'aria-label': t('common.delete') + ' · ' + wish.title,
94
+ title: t('common.delete'),
95
+ onClick: () => removeWish(wish),
96
+ }, createElement(IconTrash))), wish.description !== undefined ? createElement('div', { className: 'xy-meta' }, wish.description) : null, wish.estimatedCompletionDate !== undefined
97
+ ? createElement('div', { className: 'xy-meta' }, t('wish.eta', { date: formatMediumDate(wish.estimatedCompletionDate) }))
98
+ : null,
99
+ // 与今日页 hero 进度条同一语义语法:读屏可感知进度(role+valuenow),不只是一根哑条
100
+ createElement('div', {
101
+ className: 'xy-bar',
102
+ role: 'progressbar',
103
+ 'aria-valuemin': 0,
104
+ 'aria-valuemax': 100,
105
+ 'aria-valuenow': wish.progress,
106
+ 'aria-label': progressLabel + pendingMeta,
107
+ }, createElement('div', { className: 'xy-bar-fill', style: { transform: `scaleX(${Math.min(wish.progress, 100) / 100})` } })), settled ? createElement('div', { className: 'xy-meta' }, t('wish.settleHint')) : null, wish.tasks.length > 0
108
+ ? createElement('div', { className: 'xy-wishtasks' }, ...wishTasks(wish))
109
+ : createElement('div', { className: 'xy-meta' }, t('wish.noTasks')));
110
+ };
102
111
  return createElement('div', { className: 'xy-page', ref: stabilize }, page.error !== undefined ? createElement(StaleBanner, { onRetry: () => void page.reload() }) : null, showNoPresetHint ? createElement('p', { className: 'xy-hint' }, t('today.noPresetHint')) : null, createElement('div', { className: 'xy-page-head' }, createElement('h2', { className: 'xy-page-title' }, t('wish.pageTitle')), createElement('span', { className: 'xy-meta' }, t('wish.summary', {
103
- active: active.length,
112
+ // 四态计数各计各的(进行中扣除计划中与待收尾,不相套);列表渲染仍归未完成组(未完成≠进行中计数)
113
+ active: active.length - settledCount - planningCount,
114
+ planning: planningCount > 0 ? t('wish.planningSuffix', { n: planningCount }) : '',
115
+ settled: settledCount > 0 ? t('wish.settledSuffix', { n: settledCount }) : '',
104
116
  achieved: achieved.length > 0 ? t('wish.achievedSuffix', { n: achieved.length }) : '',
105
117
  })),
106
118
  // 动作组整体右置(单一 margin-left:auto),避免多按钮各自 auto margin 平分剩余空间
package/lib/client.js CHANGED
@@ -61,11 +61,17 @@ const ZH_DICT = {
61
61
  "state.deleted": "已删除「{title}」",
62
62
  "state.done": "已完成",
63
63
  "wish.progress": "进度 {percent}%",
64
- "wish.progressHasPending": "含未领取任务",
64
+ "wish.planning": "计划中",
65
+ "wish.achieved": "已达成 🎉",
66
+ "wish.pendingCount": "{n} 个任务待领取",
67
+ "wish.settled": "待收尾",
68
+ "wish.settleHint": "答应自己的都做完了——待领取的领了继续,或删掉就达成",
65
69
  "wish.eta": "预计完成 {date}",
66
70
  "wish.noTasks": "暂无下属任务——在对话里告诉我,让我为它推荐打卡任务",
67
71
  "wish.pageTitle": "我的愿望",
68
- "wish.summary": "{active} 个进行中{achieved}",
72
+ "wish.summary": "{active} 个进行中{planning}{settled}{achieved}",
73
+ "wish.planningSuffix": " · {n} 个计划中",
74
+ "wish.settledSuffix": " · {n} 个待收尾",
69
75
  "wish.achievedSuffix": " · {n} 个已达成",
70
76
  "wish.sectionAchieved": "已达成",
71
77
  "wish.empty.title": "还没有愿望",
@@ -332,10 +338,9 @@ const ZH_DICT = {
332
338
  "chart.subtitle.byTaskTopN": "按任务 TopN",
333
339
  "chart.subtitle.byWishTopN": "按愿望 TopN",
334
340
  "chart.subtitle.byHourBucket": "按实际打卡时刻分桶;过滤范围为打卡日区间",
335
- "chart.subtitle.progressPercent": "%(应打天数完成率)",
336
- "chart.subtitle.progressPercentPending": "%(应打天数完成率,含未领取任务)",
341
+ "chart.subtitle.claimedPercent": "%(已领取任务完成率)",
337
342
  "chart.subtitle.lastDays": "近 {n} 天",
338
- "chart.subtitle.daysInclPending": "{done}/{due} 天(含未领取任务)",
343
+ "chart.subtitle.daysClaimed": "{done}/{due} 天(已领取任务)",
339
344
  "chart.subtitle.daysRatio": "{done}/{due} 天",
340
345
  "chart.subtitle.streak": "当前 {current} 天 / 最长 {best} 天",
341
346
  "detail.grid.title": "打卡记录",
@@ -462,11 +467,17 @@ const EN_DICT = {
462
467
  "state.deleted": "Deleted “{title}”",
463
468
  "state.done": "Done",
464
469
  "wish.progress": "Progress {percent}%",
465
- "wish.progressHasPending": "includes unclaimed tasks",
470
+ "wish.planning": "Planning",
471
+ "wish.achieved": "Achieved 🎉",
472
+ "wish.pendingCount": "{n} to claim",
473
+ "wish.settled": "Wrapping up",
474
+ "wish.settleHint": "Everything you committed to is done — claim the rest to keep going, or delete them to achieve the wish",
466
475
  "wish.eta": "Target date {date}",
467
476
  "wish.noTasks": "No tasks yet — tell me in chat and I will suggest check-in tasks for it",
468
477
  "wish.pageTitle": "My Wishes",
469
- "wish.summary": "{active} in progress{achieved}",
478
+ "wish.summary": "{active} in progress{planning}{settled}{achieved}",
479
+ "wish.planningSuffix": " · {n} planning",
480
+ "wish.settledSuffix": " · {n} wrapping up",
470
481
  "wish.achievedSuffix": " · {n} achieved",
471
482
  "wish.sectionAchieved": "Achieved",
472
483
  "wish.empty.title": "No wishes yet",
@@ -733,10 +744,9 @@ const EN_DICT = {
733
744
  "chart.subtitle.byTaskTopN": "By task (top N)",
734
745
  "chart.subtitle.byWishTopN": "By wish (top N)",
735
746
  "chart.subtitle.byHourBucket": "Bucketed by actual check-in time; filtered to the check-in date range",
736
- "chart.subtitle.progressPercent": "% of scheduled-day completion",
737
- "chart.subtitle.progressPercentPending": "% of scheduled-day completion (incl. unclaimed)",
747
+ "chart.subtitle.claimedPercent": "% of committed-task completion",
738
748
  "chart.subtitle.lastDays": "Last {n} days",
739
- "chart.subtitle.daysInclPending": "{done}/{due} days (incl. unclaimed)",
749
+ "chart.subtitle.daysClaimed": "{done}/{due} days (claimed tasks)",
740
750
  "chart.subtitle.daysRatio": "{done}/{due} days",
741
751
  "chart.subtitle.streak": "Current {current}d / best {best}d",
742
752
  "detail.grid.title": "Check-in record",
@@ -2107,20 +2117,19 @@ const CHART_SUBTITLE_KEYS = {
2107
2117
  "按任务 TopN": "chart.subtitle.byTaskTopN",
2108
2118
  "按愿望 TopN": "chart.subtitle.byWishTopN",
2109
2119
  "按实际打卡时刻分桶;过滤范围为打卡日区间": "chart.subtitle.byHourBucket",
2110
- "%(应打天数完成率)": "chart.subtitle.progressPercent",
2111
- "%(应打天数完成率,含未领取任务)": "chart.subtitle.progressPercentPending"
2120
+ "%(已领取任务完成率)": "chart.subtitle.claimedPercent"
2112
2121
  };
2113
2122
  /**
2114
2123
  * 副标题本地化:固定短语查表;含数字的模板(近 N 天 / A/B 天 / 当前 A 天 / 最长 B 天 /
2115
- * 含未领取任务)按正则重排——服务端模板变化时未知副标题原样回显(诚实降级)。
2124
+ * 已领取任务口径)按正则重排——服务端模板变化时未知副标题原样回显(诚实降级)。
2116
2125
  */
2117
2126
  function localizeChartSubtitle(subtitle) {
2118
2127
  const fixed = CHART_SUBTITLE_KEYS[subtitle];
2119
2128
  if (fixed !== void 0) return t(fixed);
2120
2129
  let m = /^近 (\d+) 天$/.exec(subtitle);
2121
2130
  if (m !== null) return t("chart.subtitle.lastDays", { n: m[1] });
2122
- m = /^(\d+)\/(\d+) 天(含未领取任务)$/.exec(subtitle);
2123
- if (m !== null) return t("chart.subtitle.daysInclPending", {
2131
+ m = /^(\d+)\/(\d+) 天(已领取任务)$/.exec(subtitle);
2132
+ if (m !== null) return t("chart.subtitle.daysClaimed", {
2124
2133
  done: m[1],
2125
2134
  due: m[2]
2126
2135
  });
@@ -2392,7 +2401,7 @@ function WishView(props) {
2392
2401
  return (0, react.createElement)("div", { className: `xy-card ${deleted ? "xy-deleted" : ""}` }, (0, react.createElement)("div", { className: "xy-card-head" }, (0, react.createElement)("span", {
2393
2402
  className: "xy-badge xy-badge-cat",
2394
2403
  style: categoryVars(wish.wish.colorKey, wish.wish.categoryName)
2395
- }, t("badge.wish")), (0, react.createElement)("span", { className: "xy-title" }, deleted ? t("state.deleted", { title: wish.wish.title }) : wish.wish.title)), (0, react.createElement)("div", { className: "xy-meta" }, deleted ? wish.wish.categoryName : `${wish.wish.categoryName} · ${t("wish.progress", { percent: wish.wish.progress })}`));
2404
+ }, t("badge.wish")), (0, react.createElement)("span", { className: "xy-title" }, deleted ? t("state.deleted", { title: wish.wish.title }) : wish.wish.title)), (0, react.createElement)("div", { className: "xy-meta" }, deleted ? wish.wish.categoryName : `${wish.wish.categoryName} · ${wish.wish.achieved ? t("wish.achieved") : wish.wish.planning ? t("wish.planning") : t("wish.progress", { percent: wish.wish.progress })}${wish.wish.pendingCount ? ` · ${t("wish.pendingCount", { n: wish.wish.pendingCount })}` : ""}${wish.wish.settled ? ` · ${t("wish.settled")}` : ""}`));
2396
2405
  }
2397
2406
  function TaskView(props) {
2398
2407
  const t = useXyT();
@@ -3660,8 +3669,10 @@ function WishesPage() {
3660
3669
  });
3661
3670
  const data = page.data;
3662
3671
  if (data === void 0) return (0, react.createElement)(PageSkeleton);
3663
- const active = data.wishes.filter((w) => !w.archived);
3664
3672
  const achieved = data.wishes.filter((w) => w.archived);
3673
+ const settledCount = data.wishes.filter((w) => w.settled).length;
3674
+ const planningCount = data.wishes.filter((w) => !w.archived && w.planning).length;
3675
+ const active = data.wishes.filter((w) => !w.archived);
3665
3676
  /** 删除愿望(级联下属任务与打卡记录,服务端同一写路径):确认后直连动作并整页刷新。
3666
3677
  * 行移除后触发按钮销毁、焦点落空——刷新完成后把焦点交给页面标题兜底。 */
3667
3678
  const removeWish = (wish) => {
@@ -3696,34 +3707,41 @@ function WishesPage() {
3696
3707
  onChanged: page.reload
3697
3708
  })] : [line];
3698
3709
  });
3699
- const card = (wish) => (0, react.createElement)("div", {
3700
- key: wish.wishId,
3701
- className: "xy-wishcard"
3702
- }, (0, react.createElement)("div", { className: "xy-card-head" }, (0, react.createElement)("span", {
3703
- className: "xy-badge xy-badge-cat",
3704
- style: categoryVars(wish.colorKey, wish.categoryName)
3705
- }, wish.categoryName), (0, react.createElement)("span", { className: "xy-title" }, wish.title), (0, react.createElement)("span", { className: "xy-progress-num" }, t("wish.progress", { percent: wish.progress }), wish.tasks.some((task) => task.status === "pending") ? (0, react.createElement)("span", { className: "xy-meta" }, ` · ${t("wish.progressHasPending")}`) : null), (0, react.createElement)("button", {
3706
- className: "xy-btn xy-btn-danger xy-btn-icon",
3707
- disabled: deleting,
3708
- "aria-label": t("common.delete") + " · " + wish.title,
3709
- title: t("common.delete"),
3710
- onClick: () => removeWish(wish)
3711
- }, (0, react.createElement)(IconTrash))), wish.description !== void 0 ? (0, react.createElement)("div", { className: "xy-meta" }, wish.description) : null, wish.estimatedCompletionDate !== void 0 ? (0, react.createElement)("div", { className: "xy-meta" }, t("wish.eta", { date: formatMediumDate(wish.estimatedCompletionDate) })) : null, (0, react.createElement)("div", {
3712
- className: "xy-bar",
3713
- role: "progressbar",
3714
- "aria-valuemin": 0,
3715
- "aria-valuemax": 100,
3716
- "aria-valuenow": wish.progress,
3717
- "aria-label": t("wish.progress", { percent: wish.progress }) + (wish.tasks.some((task) => task.status === "pending") ? ` · ${t("wish.progressHasPending")}` : "")
3718
- }, (0, react.createElement)("div", {
3719
- className: "xy-bar-fill",
3720
- style: { transform: `scaleX(${Math.min(wish.progress, 100) / 100})` }
3721
- })), wish.tasks.length > 0 ? (0, react.createElement)("div", { className: "xy-wishtasks" }, ...wishTasks(wish)) : (0, react.createElement)("div", { className: "xy-meta" }, t("wish.noTasks")));
3710
+ const card = (wish) => {
3711
+ const { pendingCount, settled, planning } = wish;
3712
+ const progressLabel = planning ? t("wish.planning") : t("wish.progress", { percent: wish.progress });
3713
+ const pendingMeta = pendingCount > 0 ? ` · ${t("wish.pendingCount", { n: pendingCount })}` : "";
3714
+ return (0, react.createElement)("div", {
3715
+ key: wish.wishId,
3716
+ className: "xy-wishcard"
3717
+ }, (0, react.createElement)("div", { className: "xy-card-head" }, (0, react.createElement)("span", {
3718
+ className: "xy-badge xy-badge-cat",
3719
+ style: categoryVars(wish.colorKey, wish.categoryName)
3720
+ }, wish.categoryName), (0, react.createElement)("span", { className: "xy-title" }, wish.title), (0, react.createElement)("span", { className: "xy-progress-num" }, progressLabel, pendingCount > 0 ? (0, react.createElement)("span", { className: "xy-meta" }, pendingMeta) : null), (0, react.createElement)("button", {
3721
+ className: "xy-btn xy-btn-danger xy-btn-icon",
3722
+ disabled: deleting,
3723
+ "aria-label": t("common.delete") + " · " + wish.title,
3724
+ title: t("common.delete"),
3725
+ onClick: () => removeWish(wish)
3726
+ }, (0, react.createElement)(IconTrash))), wish.description !== void 0 ? (0, react.createElement)("div", { className: "xy-meta" }, wish.description) : null, wish.estimatedCompletionDate !== void 0 ? (0, react.createElement)("div", { className: "xy-meta" }, t("wish.eta", { date: formatMediumDate(wish.estimatedCompletionDate) })) : null, (0, react.createElement)("div", {
3727
+ className: "xy-bar",
3728
+ role: "progressbar",
3729
+ "aria-valuemin": 0,
3730
+ "aria-valuemax": 100,
3731
+ "aria-valuenow": wish.progress,
3732
+ "aria-label": progressLabel + pendingMeta
3733
+ }, (0, react.createElement)("div", {
3734
+ className: "xy-bar-fill",
3735
+ style: { transform: `scaleX(${Math.min(wish.progress, 100) / 100})` }
3736
+ })), settled ? (0, react.createElement)("div", { className: "xy-meta" }, t("wish.settleHint")) : null, wish.tasks.length > 0 ? (0, react.createElement)("div", { className: "xy-wishtasks" }, ...wishTasks(wish)) : (0, react.createElement)("div", { className: "xy-meta" }, t("wish.noTasks")));
3737
+ };
3722
3738
  return (0, react.createElement)("div", {
3723
3739
  className: "xy-page",
3724
3740
  ref: stabilize
3725
3741
  }, page.error !== void 0 ? (0, react.createElement)(StaleBanner, { onRetry: () => void page.reload() }) : null, showNoPresetHint ? (0, react.createElement)("p", { className: "xy-hint" }, t("today.noPresetHint")) : null, (0, react.createElement)("div", { className: "xy-page-head" }, (0, react.createElement)("h2", { className: "xy-page-title" }, t("wish.pageTitle")), (0, react.createElement)("span", { className: "xy-meta" }, t("wish.summary", {
3726
- active: active.length,
3742
+ active: active.length - settledCount - planningCount,
3743
+ planning: planningCount > 0 ? t("wish.planningSuffix", { n: planningCount }) : "",
3744
+ settled: settledCount > 0 ? t("wish.settledSuffix", { n: settledCount }) : "",
3727
3745
  achieved: achieved.length > 0 ? t("wish.achievedSuffix", { n: achieved.length }) : ""
3728
3746
  })), (0, react.createElement)("div", { className: "xy-page-actions" }, (0, react.createElement)("button", {
3729
3747
  className: "xy-btn",
package/lib/growth.js CHANGED
@@ -117,7 +117,9 @@ export function resolveLevel(totalExperience) {
117
117
  export function growthSummary(store, today = todayIso()) {
118
118
  // 单遍任务索引批量新鲜化(freshWishes),替代逐愿望嵌套全表扫描 O(W×T)
119
119
  const wishes = freshWishes(store);
120
- const completedWishes = wishes.filter((wish) => wish.progress >= 100).length;
120
+ // 达成口径与愿望卡一致(freshWishes 派生的 archived 谓词):承诺全部完成且无未处理候选——
121
+ // 禁止在此重复写 progress >= 100 谓词(候选拦达成会漏掉,见 §5.2 规则 7)
122
+ const completedWishes = wishes.filter((wish) => wish.archived).length;
121
123
  let totalTasks = 0;
122
124
  let completedTasks = 0;
123
125
  for (const [, task] of store.domain.table('tasks').entries()) {
@@ -1,4 +1,4 @@
1
- import { anchorOf, checkinCountIndex, freshTask, freshWishes, isClaimed } from '../store.js';
1
+ import { anchorOf, checkinCountIndex, claimedDaysSum, freshTask, freshWishes, isClaimed } from '../store.js';
2
2
  import { computeCheckInStats } from '../growth.js';
3
3
  import { calculateOpportunityDates, todayIso } from '../opportunity.js';
4
4
  export const CHART_KEYS = [
@@ -137,23 +137,17 @@ export function buildChart(key, params, store, config, today = todayIso()) {
137
137
  };
138
138
  }
139
139
  case 'taskCompletionRate': {
140
- let required = 0;
141
- let completed = 0;
142
- let hasPending = false;
143
- for (const task of tasksOf(store, params.wishId)) {
144
- required += task.requiredDays;
145
- completed += task.completedDays;
146
- if (!isClaimed(task))
147
- hasPending = true;
148
- }
140
+ // 承诺口径(§5.2 规则 7):分母只计已领取任务的应打天数——候选不进比率、不拖累完成率,
141
+ // 求和走 store.claimedDaysSum(与愿望进度公式族单一收口,消费侧不重复 isClaimed 聚合)
142
+ const { required, completed } = claimedDaysSum(tasksOf(store, params.wishId));
149
143
  if (required <= 0)
150
144
  return undefined;
151
145
  const ratio = completed / required;
152
146
  return {
153
147
  chartKey: key,
154
148
  title: params.title ?? '任务完成率',
155
- // 口径透明(诚实标注惯例):分母含未领取任务的应打天数,与愿望进度同一公式
156
- subtitle: `${completed}/${required} 天${hasPending ? '(含未领取任务)' : ''}`,
149
+ // 口径自述:副标题恒标注分母范围,读图无需区分有无候选(候选可见性由愿望卡与任务页承担)
150
+ subtitle: `${completed}/${required} 天(已领取任务)`,
157
151
  chartType: 'arcbars',
158
152
  data: [{ label: '完成率', value: completed, ratio }],
159
153
  };
@@ -308,18 +302,16 @@ function buildTailChart(key, params, store, today, days, limit, config) {
308
302
  return undefined;
309
303
  const shown = wishes.slice().sort((a, b) => b.progress - a.progress).slice(0, limit);
310
304
  const data = shown.map((wish) => ({ label: wish.title, value: wish.progress }));
311
- // 长期尺度 = 计划口径(§5.2 规则 7):分母含未领取任务的应打天数——愿望着含
312
- // 未领取任务时副标题动态标注,与 taskCompletionRate 同款诚实口径
313
- const pendingWishIds = new Set(tasksOf(store).filter((task) => !isClaimed(task))
314
- .map((task) => task.wishId).filter((id) => id !== undefined));
315
- const hasPending = shown.some((wish) => pendingWishIds.has(wish.wishId));
316
- return { chartKey: key, title: params.title ?? '愿望进度', subtitle: hasPending ? '%(应打天数完成率,含未领取任务)' : '%(应打天数完成率)', chartType: 'bar', data };
305
+ // 承诺口径(§5.2 规则 7):分母只计已领取任务,候选不进比率、不拖累进度;
306
+ // 副标题恒标注口径,候选的可见性由愿望卡候选行与任务页待领取组承担
307
+ return { chartKey: key, title: params.title ?? '愿望进度', subtitle: '%(已领取任务完成率)', chartType: 'bar', data };
317
308
  }
318
309
  if (key === 'wishAchievement') {
319
310
  const wishes = freshWishes(store);
320
311
  if (!wishes.length)
321
312
  return undefined;
322
- const achieved = wishes.filter((wish) => wish.progress >= 100).length;
313
+ // 达成口径与愿望卡一致(freshWishes 派生的 archived 谓词):承诺全部完成且无未处理候选
314
+ const achieved = wishes.filter((wish) => wish.archived).length;
323
315
  return {
324
316
  chartKey: key,
325
317
  title: params.title ?? '愿望达成率',
@@ -1,4 +1,4 @@
1
- import { CYCLE_LABELS, isClaimed, planForDay } from '../store.js';
1
+ import { CYCLE_LABELS, freshWishes, isClaimed, planForDay } from '../store.js';
2
2
  import { todayIso } from '../opportunity.js';
3
3
  /** 未领取任务单列的用户可见文案:提示词上下文与工具输出共用同一份,防口径漂移(§5.2 规则 7)。 */
4
4
  export const UNCLAIMED_NOTE = '可领取后打卡;是否领取由用户决定,不得主动领取';
@@ -304,6 +304,13 @@ const WISH_GUIDE = `# 愿望操作指南
304
304
  - 用户未提供预计完成日期时:按主题推算合理期限(语言/乐器/学习类 3-6 个月,健身/健康类 2-3 个月,工作类 3 个月,具体事项类 1 个月),格式 yyyy-MM-dd
305
305
  - 用户已明确提供的信息(日期、描述)必须优先使用,不得覆盖;只在缺失时补全
306
306
 
307
+ ## 进度与达成规则(转述数字时如实说明)
308
+ - 愿望进度 = 已领取任务的应打天数完成率;待领取的任务不算进进度——不拖累完成率,删除待领取的任务也不抬高进度
309
+ - 愿望达成 = 进度 100% 且没有待领取任务:都做完但还有待领取的任务时,愿望停在「待收尾」(进度条满了,先不归档)——向用户说明并让其决定:领了继续,或删掉就达成
310
+ - 不得为促成达成主动领取或删除任务(领取不可逆,删除是计划修订,都是用户的决定)
311
+ - 还没开始的愿望显示「计划中」;进度只反映已承诺的部分,待领取的任务在任务页「待领取」组可见
312
+ - 对用户说话用日常用语:统一说「待领取的任务」,不说「候选」「待结算」「口径」「锚点」这类内部词
313
+
307
314
  ## 注意事项
308
315
  - 分类名称优先2-3个中文字符(如:学习、健康、工作)`;
309
316
  const TASK_GUIDE = `# 任务操作指南
@@ -325,6 +332,8 @@ const TASK_GUIDE = `# 任务操作指南
325
332
  ## 状态流转
326
333
  pending(待领取)→ claim_task → in_progress(进行中)→ check_in 达标 → closed(完结)
327
334
  截止日过期未达标也会关闭(closed/expired);对已过期任务延长截止日即触发重新开始。
335
+ - 领取即扩大承诺范围:任务的应打天数计入所属愿望进度(进度只统计已领取任务),进度可能因此回落——回包会注明,如实转述
336
+ - 待领取的任务不计入愿望进度、不拖累完成率,但进度满了而还有待领取的任务时愿望不达成(停在「待收尾」,等用户决定领取或删除收尾)
328
337
 
329
338
  ## 字段自动补全
330
339
  - 用户未指定截止日期时:按周期推算合理日期(daily→约30天后、weekly→8周后、monthly→3个月后、once→2周内),格式 yyyy-MM-dd 且不早于今天
@@ -381,7 +390,7 @@ const CHART_GUIDE = `# 图表展示指南(均为内部能力,通过 generate
381
390
  - 「一般几点打卡/打卡时间规律」→ chartKey=checkinTimeDistribution(雷达,按时段)
382
391
  - 「周几打卡最多/周活跃规律」→ chartKey=weeklyActivity(雷达,按星期)
383
392
 
384
- ## 数据口径与相互区别(易混淆图表,按维度选)
393
+ ## 数据规则与相互区别(易混淆图表,按维度选)
385
394
  - checkinTrend vs checkinRateTrend:前者每日打卡次数,后者每日完成率(已打卡/应打卡)
386
395
  - checkinTrend vs checkinCalendar:前者近N天逐日折线,后者自然日网格热力图
387
396
  - checkinByCategory vs wishCategory:前者打卡次数按分类,后者愿望数量按分类
@@ -390,9 +399,9 @@ const CHART_GUIDE = `# 图表展示指南(均为内部能力,通过 generate
390
399
  - wishProgress vs wishAchievement:前者逐愿望进度排行(条形),后者整体达成率(环状)
391
400
  - checkinTimeDistribution vs weeklyActivity:前者一天内时段分布,后者星期几分布
392
401
 
393
- ## 统计口径(回复涉及数字时按此措辞,不臆测)
402
+ ## 统计规则(回复涉及数字时按此措辞,不臆测)
394
403
  - 统计类图表只统计今天(含)以前的打卡;提前勾选的未来日期不计入统计,只体现在打卡日历热力图上
395
- - taskCompletionRate 分母含未领取任务的应打天数(与愿望进度同一公式),副标题会标注口径——解读时如实转述
404
+ - taskCompletionRate 与 wishProgress 都只算已领取任务的应打天数(与愿望进度同一规则),待领取的不算进也不拖累完成率,副标题恒标注「已领取任务」——转述时说「只算已领取的任务」
396
405
  - checkinRateTrend 的无安排日(当日没有应打卡任务)不产出比率点,不代表完成率为 0%
397
406
  - checkinTrend/checkinCalendar 的取消打卡会即时反映(记录已撤销);但已生成的图表卡是生成时刻的快照,回放时以卡片标注的「生成于」为准,需要最新数据应重新生成
398
407
 
@@ -486,7 +495,11 @@ export function registerPrompts(ctx, config) {
486
495
  const due = claimed.filter((item) => !item.checked);
487
496
  const done = claimed.length - due.length;
488
497
  const pending = plan.items.filter((item) => !isClaimed(item.task));
489
- const openWishes = [...store.domain.table('wishes').entries()].map(([, w]) => w).filter((w) => !w.archived);
498
+ // 承诺口径读侧统一走 freshWishes:库存 progress/archived 只在写路径刷新,
499
+ // 达成闸门(候选拦达成)变更后库存位可与派生面分叉——上下文与列表/页面必须同源
500
+ const wishViews = freshWishes(store);
501
+ const openWishes = wishViews.filter((w) => !w.archived);
502
+ const settledWishes = wishViews.filter((w) => w.settled);
490
503
  const parts = [`<today_context>`, `今天是 ${today}。`];
491
504
  if (claimed.length > 0) {
492
505
  parts.push(`今日打卡进度:${done}/${claimed.length}。`);
@@ -507,7 +520,11 @@ export function registerPrompts(ctx, config) {
507
520
  }
508
521
  }
509
522
  if (openWishes.length > 0)
510
- parts.push(`进行中的愿望 ${openWishes.length} 个。`);
523
+ parts.push(`进行中的愿望 ${openWishes.length} 个${settledWishes.length > 0 ? `(其中 ${settledWishes.length} 个待收尾:答应自己的都做完了,还有任务待领取,等用户决定领了继续还是删掉达成)` : ''}。`);
524
+ // 全达成与从零两态分开:曾一律说「还没有任何愿望」,完成全部愿望的老用户
525
+ // 被模型当新客介绍产品、被引导说「第一个愿望」,如实说成就再谈下一步
526
+ else if (wishViews.length > 0)
527
+ parts.push(`用户的 ${wishViews.length} 个愿望均已达成:先肯定这份坚持,再自然地邀请许下一个新愿望。`);
511
528
  else
512
529
  parts.push('用户还没有任何愿望:新会话请友好自我介绍(你是星愿AI助手,帮用户拆解愿望、制定计划、坚持打卡),引导说出第一个愿望;已有会话则自然衔接。');
513
530
  parts.push('</today_context>');