incremnt 0.7.2 → 0.8.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/README.md +57 -1
- package/package.json +2 -1
- package/src/ask-answer-verifier.js +857 -0
- package/src/ask-coach.js +2634 -0
- package/src/ask-replay.js +358 -0
- package/src/auth.js +169 -15
- package/src/contract.js +160 -3
- package/src/format.js +24 -1
- package/src/lib.js +205 -17
- package/src/mcp.js +88 -24
- package/src/openrouter.js +242 -19
- package/src/plan-changeset.js +132 -0
- package/src/program-draft.js +230 -0
- package/src/prompt-changelog.js +90 -0
- package/src/promptfoo-evals.js +10 -4
- package/src/promptfoo-langfuse-scores.js +55 -0
- package/src/queries.js +992 -987
- package/src/remote.js +465 -12
- package/src/score-context.js +14 -7
- package/src/score-prelude.js +113 -0
- package/src/service-url.js +9 -0
- package/src/summary-evals.js +677 -42
- package/src/sync-service.js +1259 -352
- package/src/transport.js +119 -3
package/src/score-context.js
CHANGED
|
@@ -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
|
|
33
|
-
recovery: '
|
|
34
|
-
stimulus: '
|
|
35
|
-
execution: '
|
|
36
|
-
progression: '
|
|
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 = '
|
|
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(`
|
|
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
|
+
}
|
package/src/service-url.js
CHANGED
|
@@ -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
|
+
}
|