incremnt 0.3.0 → 0.5.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/src/remote.js CHANGED
@@ -39,7 +39,9 @@ const remoteCommandHandlers = {
39
39
  'health-ai': executeRemoteRead,
40
40
  'training-load': executeRemoteRead,
41
41
  'ask-history': executeRemoteRead,
42
- 'ask-show': executeRemoteRead
42
+ 'ask-show': executeRemoteRead,
43
+ 'program-share-fetch': executeRemoteRead,
44
+ 'increment-score-history': executeRemoteRead
43
45
  };
44
46
 
45
47
  async function executeRemoteRead(options, sessionState, normalizedCommand) {
@@ -158,6 +160,15 @@ function endpointForCommand(baseUrl, normalizedCommand, options) {
158
160
  }
159
161
  case 'ask-show':
160
162
  return resolveServiceUrl(baseUrl, `/cli/ask/history/${options.id}`);
163
+ case 'program-share-fetch':
164
+ return resolveServiceUrl(baseUrl, `/program-share/${options.token}`);
165
+ case 'increment-score-history': {
166
+ const url = resolveServiceUrl(baseUrl, '/mobile/score-snapshots');
167
+ if (options.from) url.searchParams.set('from', options.from);
168
+ if (options.to) url.searchParams.set('to', options.to);
169
+ if (options.limit) url.searchParams.set('limit', options.limit);
170
+ return url;
171
+ }
161
172
  default:
162
173
  return resolveServiceUrl(baseUrl, '/');
163
174
  }
@@ -180,6 +191,10 @@ function resourceNotFoundMessage(normalizedCommand, options) {
180
191
  return `Conversation not found: ${options.id}`;
181
192
  }
182
193
 
194
+ if (normalizedCommand === 'program-share-fetch') {
195
+ return `Program share not found: ${options.token}`;
196
+ }
197
+
183
198
  return 'Requested resource was not found.';
184
199
  }
185
200
 
@@ -278,6 +293,134 @@ const remoteWriteCommandHandlers = {
278
293
  throw error;
279
294
  }
280
295
 
296
+ return response.json();
297
+ },
298
+
299
+ 'program-share-create': async (options, sessionState) => {
300
+ const baseUrl = sessionState.session?.transport?.baseUrl;
301
+ if (!baseUrl) throw notImplementedError();
302
+ if (!options['program-id']) {
303
+ const error = new Error('--program-id is required for programs share create.');
304
+ error.code = 'MISSING_OPTION';
305
+ throw error;
306
+ }
307
+
308
+ const endpoint = resolveServiceUrl(baseUrl, `/cli/programs/${options['program-id']}/share`);
309
+ const response = await fetch(endpoint, {
310
+ method: 'POST',
311
+ headers: {
312
+ Authorization: `Bearer ${sessionState.session?.auth?.accessToken ?? ''}`
313
+ }
314
+ });
315
+
316
+ if (response.status === 401 || response.status === 403) throw authenticationFailedError();
317
+ if (response.status === 404) {
318
+ const error = new Error(`Program not found: ${options['program-id']}`);
319
+ error.code = 'REMOTE_NOT_FOUND';
320
+ throw error;
321
+ }
322
+ if (!response.ok) {
323
+ const payload = await response.json().catch(() => null);
324
+ const error = new Error(payload?.error ?? `Unexpected error (HTTP ${response.status}).`);
325
+ error.code = 'REMOTE_HTTP_ERROR';
326
+ throw error;
327
+ }
328
+ return response.json();
329
+ },
330
+
331
+ 'program-share-list': async (options, sessionState) => {
332
+ const baseUrl = sessionState.session?.transport?.baseUrl;
333
+ if (!baseUrl) throw notImplementedError();
334
+ if (!options['program-id']) {
335
+ const error = new Error('--program-id is required for programs share list.');
336
+ error.code = 'MISSING_OPTION';
337
+ throw error;
338
+ }
339
+
340
+ const endpoint = resolveServiceUrl(baseUrl, `/cli/programs/${options['program-id']}/shares`);
341
+ const response = await fetch(endpoint, {
342
+ headers: {
343
+ Authorization: `Bearer ${sessionState.session?.auth?.accessToken ?? ''}`
344
+ }
345
+ });
346
+
347
+ if (response.status === 401 || response.status === 403) throw authenticationFailedError();
348
+ if (!response.ok) {
349
+ const payload = await response.json().catch(() => null);
350
+ const error = new Error(payload?.error ?? `Unexpected error (HTTP ${response.status}).`);
351
+ error.code = 'REMOTE_HTTP_ERROR';
352
+ throw error;
353
+ }
354
+ return response.json();
355
+ },
356
+
357
+ 'increment-score-upload': async (options, sessionState) => {
358
+ const baseUrl = sessionState.session?.transport?.baseUrl;
359
+ if (!baseUrl) throw notImplementedError();
360
+ if (!options.file) {
361
+ const error = new Error('--file is required for increment-score upload.');
362
+ error.code = 'MISSING_OPTION';
363
+ throw error;
364
+ }
365
+
366
+ const raw = await fs.readFile(options.file, 'utf8');
367
+ const body = JSON.parse(raw);
368
+ if (!body || !Array.isArray(body.snapshots)) {
369
+ const error = new Error('Invalid file: expected an object with a snapshots array.');
370
+ error.code = 'INVALID_PAYLOAD';
371
+ throw error;
372
+ }
373
+
374
+ const endpoint = resolveServiceUrl(baseUrl, '/mobile/score-snapshots');
375
+ const response = await fetch(endpoint, {
376
+ method: 'POST',
377
+ headers: {
378
+ 'Content-Type': 'application/json',
379
+ Authorization: `Bearer ${sessionState.session?.auth?.accessToken ?? ''}`
380
+ },
381
+ body: JSON.stringify(body)
382
+ });
383
+
384
+ if (response.status === 401 || response.status === 403) throw authenticationFailedError();
385
+ if (!response.ok) {
386
+ const payload = await response.json().catch(() => null);
387
+ const error = new Error(payload?.error ?? `Unexpected error (HTTP ${response.status}).`);
388
+ error.code = 'REMOTE_HTTP_ERROR';
389
+ throw error;
390
+ }
391
+
392
+ return response.json();
393
+ },
394
+
395
+ 'program-share-revoke': async (options, sessionState) => {
396
+ const baseUrl = sessionState.session?.transport?.baseUrl;
397
+ if (!baseUrl) throw notImplementedError();
398
+ if (!options['share-id']) {
399
+ const error = new Error('--share-id is required for programs share revoke.');
400
+ error.code = 'MISSING_OPTION';
401
+ throw error;
402
+ }
403
+
404
+ const endpoint = resolveServiceUrl(baseUrl, `/cli/program-share/${options['share-id']}/revoke`);
405
+ const response = await fetch(endpoint, {
406
+ method: 'POST',
407
+ headers: {
408
+ Authorization: `Bearer ${sessionState.session?.auth?.accessToken ?? ''}`
409
+ }
410
+ });
411
+
412
+ if (response.status === 401 || response.status === 403) throw authenticationFailedError();
413
+ if (response.status === 404) {
414
+ const error = new Error(`Program share not found: ${options['share-id']}`);
415
+ error.code = 'REMOTE_NOT_FOUND';
416
+ throw error;
417
+ }
418
+ if (!response.ok) {
419
+ const payload = await response.json().catch(() => null);
420
+ const error = new Error(payload?.error ?? `Unexpected error (HTTP ${response.status}).`);
421
+ error.code = 'REMOTE_HTTP_ERROR';
422
+ throw error;
423
+ }
281
424
  return response.json();
282
425
  }
283
426
  };
package/src/state.js CHANGED
@@ -4,12 +4,19 @@ import path from 'node:path';
4
4
 
5
5
  export const sessionSchemaVersion = 1;
6
6
 
7
+ // Prefer HOME env over os.homedir() so test fixtures and explicit overrides
8
+ // work consistently across platforms. On Linux, os.homedir() reads from
9
+ // /etc/passwd via getpwuid and ignores HOME, breaking tests that override HOME.
10
+ export function userHomeDir() {
11
+ return process.env.HOME || os.homedir();
12
+ }
13
+
7
14
  function fallbackConfigRoot() {
8
15
  if (process.platform === 'darwin') {
9
- return path.join(os.homedir(), 'Library', 'Application Support');
16
+ return path.join(userHomeDir(), 'Library', 'Application Support');
10
17
  }
11
18
 
12
- return path.join(os.homedir(), '.config');
19
+ return path.join(userHomeDir(), '.config');
13
20
  }
14
21
 
15
22
  export function resolveConfigDir() {
@@ -0,0 +1,171 @@
1
+ function withPassRate(entry) {
2
+ return {
3
+ ...entry,
4
+ passRate: entry.total > 0 ? entry.passed / entry.total : 0
5
+ };
6
+ }
7
+
8
+ export function summarizeResults(results) {
9
+ const counts = {
10
+ total: results.length,
11
+ passed: results.filter((result) => result.passed).length
12
+ };
13
+ counts.failed = counts.total - counts.passed;
14
+ counts.passRate = counts.total > 0 ? counts.passed / counts.total : 0;
15
+ return counts;
16
+ }
17
+
18
+ function summarizeByKey(results, keyFn) {
19
+ const grouped = new Map();
20
+ for (const result of results) {
21
+ const key = keyFn(result);
22
+ const entry = grouped.get(key) ?? { total: 0, passed: 0, failed: 0 };
23
+ entry.total += 1;
24
+ if (result.passed) entry.passed += 1;
25
+ else entry.failed += 1;
26
+ grouped.set(key, entry);
27
+ }
28
+
29
+ return Object.fromEntries(
30
+ [...grouped.entries()]
31
+ .sort(([left], [right]) => left.localeCompare(right))
32
+ .map(([key, entry]) => [key, withPassRate(entry)])
33
+ );
34
+ }
35
+
36
+ export function summarizeBySurface(results) {
37
+ return summarizeByKey(results, (result) => result.surface);
38
+ }
39
+
40
+ function normalizeGeneratedDate(generatedAt) {
41
+ if (typeof generatedAt !== 'string' || generatedAt.trim().length === 0) {
42
+ return 'legacy';
43
+ }
44
+ return generatedAt.slice(0, 10);
45
+ }
46
+
47
+ function metadataValue(value) {
48
+ return typeof value === 'string' && value.trim().length > 0 ? value : 'legacy';
49
+ }
50
+
51
+ function summarizeMetadata(results) {
52
+ return {
53
+ byPromptVersion: summarizeByKey(results, (result) => metadataValue(result.metadata?.promptVersion)),
54
+ byModel: summarizeByKey(results, (result) => metadataValue(result.metadata?.model)),
55
+ byGeneratedDate: summarizeByKey(results, (result) => normalizeGeneratedDate(result.metadata?.generatedAt)),
56
+ byGitSha: summarizeByKey(results, (result) => metadataValue(result.metadata?.gitSha)),
57
+ byCohort: summarizeByKey(results, (result) => {
58
+ const promptVersion = metadataValue(result.metadata?.promptVersion);
59
+ const model = metadataValue(result.metadata?.model);
60
+ const generatedDate = normalizeGeneratedDate(result.metadata?.generatedAt);
61
+ return `${result.surface} / ${promptVersion} / ${model} / ${generatedDate}`;
62
+ })
63
+ };
64
+ }
65
+
66
+ function summarizeStoredResults(results) {
67
+ return {
68
+ summary: summarizeResults(results),
69
+ bySurface: summarizeBySurface(results),
70
+ metadata: summarizeMetadata(results)
71
+ };
72
+ }
73
+
74
+ export function buildStoredSummaryReport(snapshotPath, results) {
75
+ const summary = summarizeStoredResults(results);
76
+ return {
77
+ snapshotPath,
78
+ summary: summary.summary,
79
+ bySurface: summary.bySurface,
80
+ metadata: summary.metadata,
81
+ results: results.map((result) => ({
82
+ id: result.id,
83
+ surface: result.surface,
84
+ passed: result.passed,
85
+ output: result.output,
86
+ metadata: result.metadata ?? null,
87
+ failedChecks: result.checks.filter((check) => !check.passed)
88
+ }))
89
+ };
90
+ }
91
+
92
+ export function summarizeBatchReports(reports) {
93
+ const results = reports.flatMap((report) => report.results ?? []);
94
+ return {
95
+ snapshotCount: reports.length,
96
+ ...summarizeStoredResults(results)
97
+ };
98
+ }
99
+
100
+ function percentage(value) {
101
+ return `${(value * 100).toFixed(1)}%`;
102
+ }
103
+
104
+ export function evaluateBatchThresholds(summary, {
105
+ minPassRate = null,
106
+ minSurfacePassRates = {}
107
+ } = {}) {
108
+ const failures = [];
109
+
110
+ if (typeof minPassRate === 'number' && summary.summary.passRate < minPassRate) {
111
+ failures.push(`Overall pass rate ${percentage(summary.summary.passRate)} is below required ${percentage(minPassRate)}.`);
112
+ }
113
+
114
+ for (const [surface, minimum] of Object.entries(minSurfacePassRates)) {
115
+ const surfaceSummary = summary.bySurface?.[surface];
116
+ if (!surfaceSummary || surfaceSummary.total === 0) continue;
117
+ if (surfaceSummary.passRate < minimum) {
118
+ failures.push(`Surface ${surface} pass rate ${percentage(surfaceSummary.passRate)} is below required ${percentage(minimum)}.`);
119
+ }
120
+ }
121
+
122
+ return failures;
123
+ }
124
+
125
+ function formatSummaryLines(entries, { limit = null } = {}) {
126
+ const lines = Object.entries(entries).map(
127
+ ([label, entry]) => `- ${label}: ${entry.passed}/${entry.total} passed (${(entry.passRate * 100).toFixed(1)}%)`
128
+ );
129
+ return limit == null ? lines : lines.slice(0, limit);
130
+ }
131
+
132
+ export function formatBatchSummaryMarkdown(summary, reports, failures = []) {
133
+ const lines = [
134
+ '# Stored Summary Eval Report',
135
+ '',
136
+ `- Snapshots: ${summary.snapshotCount}`,
137
+ `- Total summaries: ${summary.summary.total}`,
138
+ `- Passed: ${summary.summary.passed}`,
139
+ `- Failed: ${summary.summary.failed}`,
140
+ `- Pass rate: ${(summary.summary.passRate * 100).toFixed(1)}%`,
141
+ '',
142
+ '## By Surface',
143
+ ...formatSummaryLines(summary.bySurface)
144
+ ];
145
+
146
+ lines.push('', '## By Prompt Version');
147
+ lines.push(...formatSummaryLines(summary.metadata.byPromptVersion));
148
+
149
+ lines.push('', '## By Model');
150
+ lines.push(...formatSummaryLines(summary.metadata.byModel));
151
+
152
+ lines.push('', '## By Generated Date');
153
+ lines.push(...formatSummaryLines(summary.metadata.byGeneratedDate));
154
+
155
+ lines.push('', '## Versioned Cohorts');
156
+ lines.push(...formatSummaryLines(summary.metadata.byCohort, { limit: 20 }));
157
+
158
+ lines.push('', '## Snapshots');
159
+ for (const report of reports) {
160
+ lines.push(`- ${report.snapshotLabel}: ${report.summary.passed}/${report.summary.total} passed`);
161
+ }
162
+
163
+ if (failures.length > 0) {
164
+ lines.push('', '## Threshold Failures');
165
+ for (const failure of failures) {
166
+ lines.push(`- ${failure}`);
167
+ }
168
+ }
169
+
170
+ return `${lines.join('\n')}\n`;
171
+ }