incremnt 0.7.2 → 0.8.1

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.
@@ -29,17 +29,24 @@ export function computeScoreBand(score) {
29
29
  // Component-keyed action templates surfaced as `recommendedNextActions` for
30
30
  // each top-2 negative driver. Keep these short, imperative, single-line.
31
31
  const COMPONENT_ACTIONS = {
32
- coverage: 'Add the missing muscle groups to your next session to close coverage gaps.',
33
- recovery: 'Prioritise sleep and an easier session to let recovery rebound.',
34
- stimulus: 'Push closer to productive weekly volume on your lagging muscle groups.',
35
- execution: 'Hit your planned sets and reps with cleaner technique next session.',
36
- progression: 'Add a small load or rep increase on your main lifts next session.'
32
+ coverage: 'Add the missing muscle work before adding more of what is already covered.',
33
+ recovery: 'Go easier next session unless sleep and soreness feel clearly better.',
34
+ stimulus: 'Put the short muscle groups earlier next time.',
35
+ execution: 'Follow the planned work more closely next session.',
36
+ progression: 'Hold jumps until the work looks repeatable.'
37
37
  };
38
38
 
39
- const GENERIC_ACTION = 'Address this driver in your next session to lift the score.';
39
+ const GENERIC_ACTION = 'Pick one thing from this note to fix next session.';
40
40
 
41
41
  function actionForDriver(driver) {
42
42
  if (!driver || typeof driver !== 'object') return GENERIC_ACTION;
43
+ const message = driverDisplayMessage(driver)?.toLowerCase() ?? '';
44
+ if (message.includes('glute')) return 'Put glutes earlier next time.';
45
+ if (/\babs\b/.test(message)) return 'Put abs before accessories next time.';
46
+ if (message.includes('calf') || message.includes('calves')) return 'Put calf work earlier next time.';
47
+ if (message.includes('missed') && message.includes('session')) return 'Move or shrink one planned day before adding more work.';
48
+ if (message.includes('dropped') && message.includes('reps')) return 'Start the first set a touch easier, or rest longer before set two.';
49
+ if (message.includes('hrv') || message.includes('sleep')) return 'Go easier next session unless sleep and HRV have bounced back.';
43
50
  const component = typeof driver.component === 'string' ? driver.component : null;
44
51
  if (component && COMPONENT_ACTIONS[component]) {
45
52
  return COMPONENT_ACTIONS[component];
@@ -123,7 +130,7 @@ export function computeSummaryText(enriched) {
123
130
  const topNeg = Array.isArray(enriched.topNegativeDrivers) ? enriched.topNegativeDrivers[0] : null;
124
131
  const topNegMessage = driverDisplayMessage(topNeg);
125
132
  if (topNegMessage) {
126
- parts.push(`Top drag: ${topNegMessage}.`);
133
+ parts.push(`Main thing: ${topNegMessage}.`);
127
134
  }
128
135
 
129
136
  const firstAction = Array.isArray(enriched.recommendedNextActions) ? enriched.recommendedNextActions[0] : null;
@@ -0,0 +1,113 @@
1
+ // Voice-safe Increment Score context for Ask Coach.
2
+ //
3
+ // Lives in its own module (no server deps) so both the live ask path
4
+ // (sync-service.js) and the eval harness (summary-evals.js) can build the same
5
+ // context the model actually reads.
6
+ //
7
+ // This block is *context the model reads*, not user-facing prose, but the model
8
+ // parrots whatever numbers it is handed. The coach-observation-voice spec
9
+ // (docs/copy/coach-observation-voice.md) puts raw component sub-scores in Tier 1
10
+ // — "never surface, anywhere" — because they are meaningless to a user. So we
11
+ // hand the model the *signal* (which area is weakest/strongest, which direction
12
+ // the score is moving, what the drivers are) without raw component numbers,
13
+ // decimals, or a bare list of daily scores it can dump back verbatim. The
14
+ // overall Score is a named feature, so the rounded headline value is allowed;
15
+ // nothing below it is.
16
+
17
+ // Maps Increment Score component keys to training-reality phrases. The bare keys
18
+ // (coverage/stimulus/execution/progression) read as engine internals; "recovery"
19
+ // is already plain language. Used only for the weakest/strongest steer.
20
+ export const SCORE_COMPONENT_PHRASES = {
21
+ coverage: 'muscle-group coverage',
22
+ stimulus: 'training stimulus',
23
+ execution: 'set execution',
24
+ progression: 'progression',
25
+ recovery: 'recovery'
26
+ };
27
+
28
+ export function scoreComponentPhrase(name) {
29
+ return SCORE_COMPONENT_PHRASES[String(name).toLowerCase()] ?? 'another training area';
30
+ }
31
+
32
+ // True when the user's question is actually about the Increment Score. Only then
33
+ // should the prelude hand the model the bare numeric headline — otherwise the
34
+ // model parrots "your score is 92/100" into answers about sessions, volume, or
35
+ // plans where it was never asked for.
36
+ export function isScoreQuestion(question) {
37
+ return /\b(?:increment\s+)?score\b/i.test(String(question ?? ''));
38
+ }
39
+
40
+ export function formatIncrementScorePrelude(snapshots, { question = '' } = {}) {
41
+ if (!Array.isArray(snapshots) || snapshots.length === 0) return null;
42
+ const latest = snapshots[0];
43
+ if (latest == null || typeof latest.score !== 'number') return null;
44
+
45
+ const lines = [
46
+ '[Increment Score — context only. Speak in training reality (recovery, fatigue, consistency, density). Never recite component values, sub-scores, decimals, or daily score numbers. Do not volunteer the overall score number unless the user asked about the score.]'
47
+ ];
48
+ // Hand over the numeric headline only when the question is score-related; the
49
+ // weakest/strongest area and direction below are always safe to provide.
50
+ if (isScoreQuestion(question)) {
51
+ lines.push(`- Current: ${Math.round(latest.score)}/100`);
52
+ }
53
+
54
+ // Component NAMES only — which area is dragging the score and which is
55
+ // carrying it — as training-reality phrases, never the underlying sub-scores
56
+ // or even the bare component keys (which read as engine internals).
57
+ if (latest.components && typeof latest.components === 'object') {
58
+ const scored = [];
59
+ for (const [name, value] of Object.entries(latest.components)) {
60
+ const num = typeof value === 'number' ? value : value?.score;
61
+ if (typeof num === 'number') scored.push({ name, num });
62
+ }
63
+ if (scored.length > 0) {
64
+ const min = Math.min(...scored.map((c) => c.num));
65
+ const max = Math.max(...scored.map((c) => c.num));
66
+ // Treat values within a few points as a tie rather than asserting a single
67
+ // weakest/strongest off an arbitrary order-dependent pick.
68
+ const TIE = 3;
69
+ const phrases = (items) => items.map((c) => scoreComponentPhrase(c.name)).join(' and ');
70
+ if (max - min <= TIE) {
71
+ lines.push('- Areas: all roughly even right now');
72
+ } else {
73
+ const weak = scored.filter((c) => c.num - min <= TIE);
74
+ const strong = scored.filter((c) => max - c.num <= TIE);
75
+ lines.push(`- Weakest: ${phrases(weak)}. Strongest: ${phrases(strong)}.`);
76
+ }
77
+ }
78
+ }
79
+
80
+ const driverLabels = (list) => {
81
+ if (!Array.isArray(list) || list.length === 0) return null;
82
+ return list
83
+ .slice(0, 3)
84
+ .map((d) => d?.label ?? d?.id ?? d?.driver)
85
+ .filter(Boolean)
86
+ .join('; ');
87
+ };
88
+ const positives = driverLabels(latest.topPositiveDrivers);
89
+ if (positives) lines.push(`- Helping the score: ${positives}`);
90
+ const negatives = driverLabels(latest.topNegativeDrivers);
91
+ if (negatives) lines.push(`- Holding the score back: ${negatives}`);
92
+
93
+ // Direction words only — no delta number, no daily-score list.
94
+ if (snapshots.length > 1) {
95
+ const prior = snapshots[1];
96
+ if (typeof prior?.score === 'number') {
97
+ const delta = latest.score - prior.score;
98
+ const dir = delta > 0 ? 'up' : delta < 0 ? 'down' : 'flat';
99
+ lines.push(`- Day-over-day: ${dir}`);
100
+ }
101
+ const recent = snapshots
102
+ .slice(0, 7)
103
+ .map((s) => (typeof s?.score === 'number' ? s.score : null))
104
+ .filter((s) => s != null);
105
+ if (recent.length >= 3) {
106
+ const span = recent[0] - recent[recent.length - 1];
107
+ const trend = span > 2 ? 'rising' : span < -2 ? 'falling' : 'steady';
108
+ lines.push(`- Last ${recent.length} days: ${trend}`);
109
+ }
110
+ }
111
+
112
+ return lines.join('\n');
113
+ }
@@ -1,3 +1,5 @@
1
+ export const DEFAULT_SERVICE_BASE_URL = 'https://incremnt-sync.onrender.com';
2
+
1
3
  export function resolveServiceUrl(baseUrl, servicePath) {
2
4
  const url = new URL(baseUrl);
3
5
  const basePath = url.pathname === '/' ? '' : url.pathname.replace(/\/+$/, '');
@@ -5,3 +7,10 @@ export function resolveServiceUrl(baseUrl, servicePath) {
5
7
  url.pathname = `${basePath}${suffix}` || '/';
6
8
  return url;
7
9
  }
10
+
11
+ export function resolveConfiguredBaseUrl(options = {}, session = null) {
12
+ if (options['base-url']) return options['base-url'];
13
+ if ('INCREMNT_BASE_URL' in process.env) return process.env.INCREMNT_BASE_URL || null;
14
+ if (session?.transport?.baseUrl) return session.transport.baseUrl;
15
+ return DEFAULT_SERVICE_BASE_URL;
16
+ }