@danceiny/gotry 0.0.1-rc.11 → 0.0.1-rc.13

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.
Files changed (37) hide show
  1. package/README.md +111 -146
  2. package/cordis.gotry-patch.yml +14 -7
  3. package/dist/capabilities/flyai.js +135 -30
  4. package/dist/capabilities/hbcli.js +14 -2
  5. package/dist/capabilities/session/benchmark.js +252 -0
  6. package/dist/capabilities/session/transport.js +25 -10
  7. package/dist/capabilities/session-consent.js +82 -0
  8. package/dist/capabilities/session-login.js +119 -0
  9. package/dist/capabilities/session-search.js +7 -4
  10. package/dist/capabilities/weather.js +72 -17
  11. package/dist/scripts/agent-reach-wrapper-tests.js +4 -0
  12. package/dist/scripts/async-collect.js +33 -5
  13. package/dist/scripts/hbcli-tests.js +25 -4
  14. package/dist/scripts/ledger-tests.js +134 -4
  15. package/dist/scripts/memory-value-report.js +379 -0
  16. package/dist/scripts/product-metrics.js +569 -0
  17. package/dist/scripts/session-benchmark.js +261 -0
  18. package/dist/scripts/session-login.js +28 -0
  19. package/dist/scripts/session-tests.js +262 -23
  20. package/dist/scripts/smoke.js +112 -14
  21. package/dist/scripts/weather-tests.js +8 -1
  22. package/dist/src/index.js +124 -5
  23. package/dist/src/loop.js +65 -14
  24. package/dist/src/state-ledger.js +30 -4
  25. package/package.json +4 -2
  26. package/ts/capabilities/flyai.ts +177 -22
  27. package/ts/capabilities/hbcli.ts +16 -4
  28. package/ts/capabilities/session/benchmark.ts +273 -0
  29. package/ts/capabilities/session/transport.ts +41 -9
  30. package/ts/capabilities/session-consent.ts +127 -0
  31. package/ts/capabilities/session-login.ts +146 -0
  32. package/ts/capabilities/session-search.ts +13 -5
  33. package/ts/capabilities/weather.ts +84 -19
  34. package/ts/scripts/async-collect.ts +48 -7
  35. package/ts/src/index.ts +107 -11
  36. package/ts/src/loop.ts +85 -12
  37. package/ts/src/state-ledger.ts +48 -4
@@ -0,0 +1,569 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ const FORMULAS = {
5
+ finalization: 'finalized_eligible_delivered_plans / eligible_delivered_plans',
6
+ nps: '100 * (promoters_9_10 - detractors_0_6) / valid_responses',
7
+ poi: 'audited_invalid_poi_claims / locked_audited_poi_claims'
8
+ };
9
+ const EXCLUSION_CODES = [
10
+ 'outside_locked_window',
11
+ 'not_invited',
12
+ 'consent_missing_or_withdrawn',
13
+ 'test_or_staff',
14
+ 'attribution_not_allowed'
15
+ ];
16
+ const M3_ACCEPTANCE = {
17
+ sampleMinimum: 50,
18
+ sampleMaximum: 200,
19
+ finalizationMinimum: 0.4,
20
+ npsMinimum: 40,
21
+ poiExclusiveMaximum: 0.01
22
+ };
23
+ const HMAC_KEY = /^hmac-sha256:[0-9a-f]{64}$/;
24
+ const SHA256 = /^[0-9a-f]{64}$/;
25
+ function object(value, label) {
26
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
27
+ throw new Error(`${label} must be an object`);
28
+ }
29
+ return value;
30
+ }
31
+ function exactKeys(value, label, keys) {
32
+ const expected = new Set(keys);
33
+ const unknown = Object.keys(value).filter((key)=>!expected.has(key));
34
+ const missing = keys.filter((key)=>!(key in value));
35
+ if (unknown.length > 0) throw new Error(`${label} contains undeclared fields: ${unknown.join(', ')}`);
36
+ if (missing.length > 0) throw new Error(`${label} missing fields: ${missing.join(', ')}`);
37
+ }
38
+ function string(value, label) {
39
+ if (typeof value !== 'string' || value.length === 0) throw new Error(`${label} must be a non-empty string`);
40
+ return value;
41
+ }
42
+ function boolean(value, label) {
43
+ if (typeof value !== 'boolean') throw new Error(`${label} must be boolean`);
44
+ return value;
45
+ }
46
+ function finiteNumber(value, label) {
47
+ if (typeof value !== 'number' || !Number.isFinite(value)) throw new Error(`${label} must be a finite number`);
48
+ return value;
49
+ }
50
+ function integer(value, label) {
51
+ const result = finiteNumber(value, label);
52
+ if (!Number.isInteger(result)) throw new Error(`${label} must be an integer`);
53
+ return result;
54
+ }
55
+ function timestamp(value, label) {
56
+ const result = string(value, label);
57
+ if (!Number.isFinite(Date.parse(result))) throw new Error(`${label} must be an ISO timestamp`);
58
+ return result;
59
+ }
60
+ function nullableTimestamp(value, label) {
61
+ if (value === null) return null;
62
+ return timestamp(value, label);
63
+ }
64
+ function nullableNps(value, label) {
65
+ if (value === null) return null;
66
+ const result = integer(value, label);
67
+ if (result < 0 || result > 10) throw new Error(`${label} must be an integer from 0 to 10`);
68
+ return result;
69
+ }
70
+ function hmacKey(value, label) {
71
+ const result = string(value, label);
72
+ if (!HMAC_KEY.test(result)) throw new Error(`${label} must be an hmac-sha256 pseudonymous key`);
73
+ return result;
74
+ }
75
+ function sha256(value, label) {
76
+ const result = string(value, label);
77
+ if (!SHA256.test(result)) throw new Error(`${label} must be a lowercase SHA-256 digest`);
78
+ return result;
79
+ }
80
+ function literal(value, label, allowed) {
81
+ const result = string(value, label);
82
+ if (!allowed.includes(result)) throw new Error(`${label} must be one of ${allowed.join(', ')}`);
83
+ return result;
84
+ }
85
+ function literalArray(value, label, allowed) {
86
+ if (!Array.isArray(value) || value.length === 0) throw new Error(`${label} must be a non-empty array`);
87
+ const result = value.map((item, index)=>literal(item, `${label}[${index}]`, allowed));
88
+ if (new Set(result).size !== result.length) throw new Error(`${label} must not contain duplicates`);
89
+ return result;
90
+ }
91
+ function exactStringArray(value, label, expected) {
92
+ if (!Array.isArray(value) || value.some((item)=>typeof item !== 'string')) {
93
+ throw new Error(`${label} must be a string array`);
94
+ }
95
+ const actual = [
96
+ ...value
97
+ ].sort();
98
+ const wanted = [
99
+ ...expected
100
+ ].sort();
101
+ if (JSON.stringify(actual) !== JSON.stringify(wanted)) {
102
+ throw new Error(`${label} must freeze exactly: ${wanted.join(', ')}`);
103
+ }
104
+ return value;
105
+ }
106
+ function requiredTrue(value, label) {
107
+ if (value !== true) throw new Error(`${label} must be true in schema v1`);
108
+ return true;
109
+ }
110
+ export function parseManifest(raw) {
111
+ const root = object(raw, 'manifest');
112
+ exactKeys(root, 'manifest', [
113
+ 'schema_version',
114
+ 'evidence_kind',
115
+ 'cohort_id',
116
+ 'locked_at',
117
+ 'window',
118
+ 'eligibility',
119
+ 'metrics',
120
+ 'nightly'
121
+ ]);
122
+ const window = object(root.window, 'manifest.window');
123
+ exactKeys(window, 'manifest.window', [
124
+ 'start_at',
125
+ 'end_at',
126
+ 'timezone'
127
+ ]);
128
+ const startAt = timestamp(window.start_at, 'manifest.window.start_at');
129
+ const endAt = timestamp(window.end_at, 'manifest.window.end_at');
130
+ if (Date.parse(startAt) > Date.parse(endAt)) throw new Error('manifest.window start_at must not be after end_at');
131
+ const eligibility = object(root.eligibility, 'manifest.eligibility');
132
+ exactKeys(eligibility, 'manifest.eligibility', [
133
+ 'sample_unit',
134
+ 'requires_invitation',
135
+ 'requires_consent',
136
+ 'allowed_attribution',
137
+ 'exclusion_codes'
138
+ ]);
139
+ const metrics = object(root.metrics, 'manifest.metrics');
140
+ exactKeys(metrics, 'manifest.metrics', [
141
+ 'sample_size',
142
+ 'finalization_rate',
143
+ 'nps',
144
+ 'poi_hallucination_rate'
145
+ ]);
146
+ const sampleSize = object(metrics.sample_size, 'manifest.metrics.sample_size');
147
+ exactKeys(sampleSize, 'manifest.metrics.sample_size', [
148
+ 'minimum',
149
+ 'maximum'
150
+ ]);
151
+ const finalization = object(metrics.finalization_rate, 'manifest.metrics.finalization_rate');
152
+ exactKeys(finalization, 'manifest.metrics.finalization_rate', [
153
+ 'formula',
154
+ 'minimum'
155
+ ]);
156
+ const nps = object(metrics.nps, 'manifest.metrics.nps');
157
+ exactKeys(nps, 'manifest.metrics.nps', [
158
+ 'formula',
159
+ 'minimum'
160
+ ]);
161
+ const poi = object(metrics.poi_hallucination_rate, 'manifest.metrics.poi_hallucination_rate');
162
+ exactKeys(poi, 'manifest.metrics.poi_hallucination_rate', [
163
+ 'formula',
164
+ 'exclusive_maximum'
165
+ ]);
166
+ const nightly = object(root.nightly, 'manifest.nightly');
167
+ exactKeys(nightly, 'manifest.nightly', [
168
+ 'requires_real_llm',
169
+ 'requires_prompt_set_sha256',
170
+ 'requires_output_sha256',
171
+ 'requires_cost_usd'
172
+ ]);
173
+ const minimum = integer(sampleSize.minimum, 'manifest.metrics.sample_size.minimum');
174
+ const maximum = integer(sampleSize.maximum, 'manifest.metrics.sample_size.maximum');
175
+ if (minimum < 1 || maximum < minimum) throw new Error('manifest sample size bounds are invalid');
176
+ const finalizationMinimum = finiteNumber(finalization.minimum, 'manifest.metrics.finalization_rate.minimum');
177
+ const npsMinimum = finiteNumber(nps.minimum, 'manifest.metrics.nps.minimum');
178
+ const poiMaximum = finiteNumber(poi.exclusive_maximum, 'manifest.metrics.poi_hallucination_rate.exclusive_maximum');
179
+ if (finalizationMinimum < 0 || finalizationMinimum > 1) throw new Error('finalization minimum must be within 0..1');
180
+ if (npsMinimum < -100 || npsMinimum > 100) throw new Error('NPS minimum must be within -100..100');
181
+ if (poiMaximum <= 0 || poiMaximum > 1) throw new Error('POI exclusive maximum must be within 0..1');
182
+ if (minimum !== M3_ACCEPTANCE.sampleMinimum || maximum !== M3_ACCEPTANCE.sampleMaximum) {
183
+ throw new Error(`M3 sample size thresholds are frozen at ${M3_ACCEPTANCE.sampleMinimum}..${M3_ACCEPTANCE.sampleMaximum}`);
184
+ }
185
+ if (finalizationMinimum !== M3_ACCEPTANCE.finalizationMinimum) {
186
+ throw new Error(`M3 finalization threshold is frozen at ${M3_ACCEPTANCE.finalizationMinimum}`);
187
+ }
188
+ if (npsMinimum !== M3_ACCEPTANCE.npsMinimum) {
189
+ throw new Error(`M3 NPS threshold is frozen at ${M3_ACCEPTANCE.npsMinimum}`);
190
+ }
191
+ if (poiMaximum !== M3_ACCEPTANCE.poiExclusiveMaximum) {
192
+ throw new Error(`M3 POI hallucination threshold is frozen at ${M3_ACCEPTANCE.poiExclusiveMaximum}`);
193
+ }
194
+ return {
195
+ schema_version: literal(root.schema_version, 'manifest.schema_version', [
196
+ 'gotry_m3_evidence_manifest_v1'
197
+ ]),
198
+ evidence_kind: literal(root.evidence_kind, 'manifest.evidence_kind', [
199
+ 'real_seed_cohort',
200
+ 'synthetic_fixture'
201
+ ]),
202
+ cohort_id: string(root.cohort_id, 'manifest.cohort_id'),
203
+ locked_at: timestamp(root.locked_at, 'manifest.locked_at'),
204
+ window: {
205
+ start_at: startAt,
206
+ end_at: endAt,
207
+ timezone: string(window.timezone, 'manifest.window.timezone')
208
+ },
209
+ eligibility: {
210
+ sample_unit: literal(eligibility.sample_unit, 'manifest.eligibility.sample_unit', [
211
+ 'unique_participant_with_eligible_delivered_plan'
212
+ ]),
213
+ requires_invitation: requiredTrue(eligibility.requires_invitation, 'manifest.eligibility.requires_invitation'),
214
+ requires_consent: requiredTrue(eligibility.requires_consent, 'manifest.eligibility.requires_consent'),
215
+ allowed_attribution: literalArray(eligibility.allowed_attribution, 'manifest.eligibility.allowed_attribution', [
216
+ 'gotry_primary',
217
+ 'gotry_assisted'
218
+ ]),
219
+ exclusion_codes: exactStringArray(eligibility.exclusion_codes, 'manifest.eligibility.exclusion_codes', EXCLUSION_CODES)
220
+ },
221
+ metrics: {
222
+ sample_size: {
223
+ minimum,
224
+ maximum
225
+ },
226
+ finalization_rate: {
227
+ formula: literal(finalization.formula, 'manifest.metrics.finalization_rate.formula', [
228
+ FORMULAS.finalization
229
+ ]),
230
+ minimum: finalizationMinimum
231
+ },
232
+ nps: {
233
+ formula: literal(nps.formula, 'manifest.metrics.nps.formula', [
234
+ FORMULAS.nps
235
+ ]),
236
+ minimum: npsMinimum
237
+ },
238
+ poi_hallucination_rate: {
239
+ formula: literal(poi.formula, 'manifest.metrics.poi_hallucination_rate.formula', [
240
+ FORMULAS.poi
241
+ ]),
242
+ exclusive_maximum: poiMaximum
243
+ }
244
+ },
245
+ nightly: {
246
+ requires_real_llm: requiredTrue(nightly.requires_real_llm, 'manifest.nightly.requires_real_llm'),
247
+ requires_prompt_set_sha256: requiredTrue(nightly.requires_prompt_set_sha256, 'manifest.nightly.requires_prompt_set_sha256'),
248
+ requires_output_sha256: requiredTrue(nightly.requires_output_sha256, 'manifest.nightly.requires_output_sha256'),
249
+ requires_cost_usd: requiredTrue(nightly.requires_cost_usd, 'manifest.nightly.requires_cost_usd')
250
+ }
251
+ };
252
+ }
253
+ export function parseCohortRecord(raw, index) {
254
+ const label = `cohort[${index}]`;
255
+ const root = object(raw, label);
256
+ exactKeys(root, label, [
257
+ 'schema_version',
258
+ 'participant_key',
259
+ 'plan_key',
260
+ 'invited',
261
+ 'consent',
262
+ 'test_or_staff',
263
+ 'attribution',
264
+ 'delivered_at',
265
+ 'finalized_at',
266
+ 'nps_score',
267
+ 'nps_recorded_at',
268
+ 'poi_audit'
269
+ ]);
270
+ const poi = object(root.poi_audit, `${label}.poi_audit`);
271
+ exactKeys(poi, `${label}.poi_audit`, [
272
+ 'locked_at',
273
+ 'locked_claims',
274
+ 'invalid_claims'
275
+ ]);
276
+ const lockedClaims = integer(poi.locked_claims, `${label}.poi_audit.locked_claims`);
277
+ const invalidClaims = integer(poi.invalid_claims, `${label}.poi_audit.invalid_claims`);
278
+ if (lockedClaims < 0 || invalidClaims < 0 || invalidClaims > lockedClaims) {
279
+ throw new Error(`${label}.poi_audit requires 0 <= invalid_claims <= locked_claims`);
280
+ }
281
+ const deliveredAt = timestamp(root.delivered_at, `${label}.delivered_at`);
282
+ const finalizedAt = nullableTimestamp(root.finalized_at, `${label}.finalized_at`);
283
+ const npsScore = nullableNps(root.nps_score, `${label}.nps_score`);
284
+ const npsRecordedAt = nullableTimestamp(root.nps_recorded_at, `${label}.nps_recorded_at`);
285
+ const poiLockedAt = timestamp(poi.locked_at, `${label}.poi_audit.locked_at`);
286
+ if (finalizedAt !== null && Date.parse(finalizedAt) < Date.parse(deliveredAt)) {
287
+ throw new Error(`${label}.finalized_at must not be before delivered_at`);
288
+ }
289
+ if (npsScore === null !== (npsRecordedAt === null)) {
290
+ throw new Error(`${label}.nps_score and nps_recorded_at must both be null or both be set`);
291
+ }
292
+ if (npsRecordedAt !== null && Date.parse(npsRecordedAt) < Date.parse(deliveredAt)) {
293
+ throw new Error(`${label}.nps_recorded_at must not be before delivered_at`);
294
+ }
295
+ if (Date.parse(poiLockedAt) < Date.parse(deliveredAt)) {
296
+ throw new Error(`${label}.poi_audit.locked_at must not be before delivered_at`);
297
+ }
298
+ return {
299
+ schema_version: literal(root.schema_version, `${label}.schema_version`, [
300
+ 'gotry_m3_cohort_record_v1'
301
+ ]),
302
+ participant_key: hmacKey(root.participant_key, `${label}.participant_key`),
303
+ plan_key: hmacKey(root.plan_key, `${label}.plan_key`),
304
+ invited: boolean(root.invited, `${label}.invited`),
305
+ consent: boolean(root.consent, `${label}.consent`),
306
+ test_or_staff: boolean(root.test_or_staff, `${label}.test_or_staff`),
307
+ attribution: literal(root.attribution, `${label}.attribution`, [
308
+ 'gotry_primary',
309
+ 'gotry_assisted'
310
+ ]),
311
+ delivered_at: deliveredAt,
312
+ finalized_at: finalizedAt,
313
+ nps_score: npsScore,
314
+ nps_recorded_at: npsRecordedAt,
315
+ poi_audit: {
316
+ locked_at: poiLockedAt,
317
+ locked_claims: lockedClaims,
318
+ invalid_claims: invalidClaims
319
+ }
320
+ };
321
+ }
322
+ export function parseNightlyRun(raw, index) {
323
+ const label = `nightly_runs[${index}]`;
324
+ const root = object(raw, label);
325
+ exactKeys(root, label, [
326
+ 'schema_version',
327
+ 'run_key',
328
+ 'executed_at',
329
+ 'real_llm',
330
+ 'prompt_set_sha256',
331
+ 'output_sha256',
332
+ 'cost_usd'
333
+ ]);
334
+ const costUsd = finiteNumber(root.cost_usd, `${label}.cost_usd`);
335
+ if (costUsd < 0) throw new Error(`${label}.cost_usd must be non-negative`);
336
+ return {
337
+ schema_version: literal(root.schema_version, `${label}.schema_version`, [
338
+ 'gotry_m3_nightly_run_v1'
339
+ ]),
340
+ run_key: hmacKey(root.run_key, `${label}.run_key`),
341
+ executed_at: timestamp(root.executed_at, `${label}.executed_at`),
342
+ real_llm: boolean(root.real_llm, `${label}.real_llm`),
343
+ prompt_set_sha256: sha256(root.prompt_set_sha256, `${label}.prompt_set_sha256`),
344
+ output_sha256: sha256(root.output_sha256, `${label}.output_sha256`),
345
+ cost_usd: costUsd
346
+ };
347
+ }
348
+ function round(value) {
349
+ return Number(value.toFixed(6));
350
+ }
351
+ function canonical(value) {
352
+ if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
353
+ if (typeof value === 'object' && value !== null) {
354
+ const record = value;
355
+ return `{${Object.keys(record).sort().map((key)=>`${JSON.stringify(key)}:${canonical(record[key])}`).join(',')}}`;
356
+ }
357
+ return JSON.stringify(value);
358
+ }
359
+ function exclusionsFor(manifest, record) {
360
+ const result = [];
361
+ const delivered = Date.parse(record.delivered_at);
362
+ if (delivered < Date.parse(manifest.window.start_at) || delivered > Date.parse(manifest.window.end_at)) result.push('outside_locked_window');
363
+ if (!record.invited) result.push('not_invited');
364
+ if (!record.consent) result.push('consent_missing_or_withdrawn');
365
+ if (record.test_or_staff) result.push('test_or_staff');
366
+ if (!manifest.eligibility.allowed_attribution.includes(record.attribution)) result.push('attribution_not_allowed');
367
+ return result;
368
+ }
369
+ export function scoreProductMetrics(manifest, cohort, nightlyRuns) {
370
+ const planKeys = new Set();
371
+ const runKeys = new Set();
372
+ for (const record of cohort){
373
+ if (planKeys.has(record.plan_key)) throw new Error(`duplicate plan_key: ${record.plan_key}`);
374
+ planKeys.add(record.plan_key);
375
+ }
376
+ for (const run of nightlyRuns){
377
+ if (runKeys.has(run.run_key)) throw new Error(`duplicate run_key: ${run.run_key}`);
378
+ runKeys.add(run.run_key);
379
+ }
380
+ const exclusionCounts = Object.fromEntries(EXCLUSION_CODES.map((code)=>[
381
+ code,
382
+ 0
383
+ ]));
384
+ const eligible = cohort.filter((record)=>{
385
+ const exclusions = exclusionsFor(manifest, record);
386
+ for (const code of exclusions)exclusionCounts[code] += 1;
387
+ return exclusions.length === 0;
388
+ });
389
+ const participants = new Set(eligible.map((record)=>record.participant_key));
390
+ const windowEnd = Date.parse(manifest.window.end_at);
391
+ const finalized = eligible.filter((record)=>record.finalized_at !== null && Date.parse(record.finalized_at) <= windowEnd).length;
392
+ const finalizationRate = eligible.length === 0 ? null : round(finalized / eligible.length);
393
+ const npsByParticipant = new Map();
394
+ for (const record of eligible){
395
+ if (record.nps_score === null || record.nps_recorded_at === null || Date.parse(record.nps_recorded_at) > windowEnd) continue;
396
+ if (npsByParticipant.has(record.participant_key)) {
397
+ throw new Error(`participant has more than one NPS response in the locked window: ${record.participant_key}`);
398
+ }
399
+ npsByParticipant.set(record.participant_key, record.nps_score);
400
+ }
401
+ const scores = [
402
+ ...npsByParticipant.values()
403
+ ];
404
+ const promoters = scores.filter((score)=>score >= 9).length;
405
+ const detractors = scores.filter((score)=>score <= 6).length;
406
+ const passives = scores.length - promoters - detractors;
407
+ const npsScore = scores.length === 0 ? null : round(100 * (promoters - detractors) / scores.length);
408
+ const lockedAudits = eligible.filter((record)=>Date.parse(record.poi_audit.locked_at) <= windowEnd);
409
+ const lockedClaims = lockedAudits.reduce((sum, record)=>sum + record.poi_audit.locked_claims, 0);
410
+ const invalidClaims = lockedAudits.reduce((sum, record)=>sum + record.poi_audit.invalid_claims, 0);
411
+ const poiRate = lockedClaims === 0 ? null : round(invalidClaims / lockedClaims);
412
+ const windowStart = Date.parse(manifest.window.start_at);
413
+ const replayableRuns = nightlyRuns.filter((run)=>{
414
+ const executedAt = Date.parse(run.executed_at);
415
+ return run.real_llm && executedAt >= windowStart && executedAt <= windowEnd;
416
+ });
417
+ const nightlyCost = round(replayableRuns.reduce((sum, run)=>sum + run.cost_usd, 0));
418
+ const samplePass = participants.size >= manifest.metrics.sample_size.minimum && participants.size <= manifest.metrics.sample_size.maximum;
419
+ const finalizationPass = finalizationRate !== null && finalizationRate >= manifest.metrics.finalization_rate.minimum;
420
+ const npsPass = npsScore !== null && npsScore >= manifest.metrics.nps.minimum;
421
+ const poiPass = poiRate !== null && poiRate < manifest.metrics.poi_hallucination_rate.exclusive_maximum;
422
+ const nightlyPass = replayableRuns.length > 0;
423
+ const metricPass = samplePass && finalizationPass && npsPass && poiPass && nightlyPass;
424
+ const businessPass = manifest.evidence_kind === 'real_seed_cohort' && metricPass;
425
+ const failedChecks = [
426
+ !samplePass && 'sample_size',
427
+ !finalizationPass && 'finalization_rate',
428
+ !npsPass && 'nps',
429
+ !poiPass && 'poi_hallucination_rate',
430
+ !nightlyPass && 'nightly_real_llm'
431
+ ].filter(Boolean);
432
+ const businessPassReason = manifest.evidence_kind === 'synthetic_fixture' ? 'evidence_kind=synthetic_fixture cannot prove business pass' : businessPass ? 'all locked M3 acceptance thresholds passed on real_seed_cohort evidence' : `real_seed_cohort acceptance gaps: ${failedChecks.join(', ')}`;
433
+ return {
434
+ schema_version: 'gotry_m3_product_metrics_summary_v1',
435
+ evidence_kind: manifest.evidence_kind,
436
+ cohort_id: manifest.cohort_id,
437
+ evidence_digest_sha256: createHash('sha256').update(canonical({
438
+ manifest,
439
+ cohort,
440
+ nightlyRuns
441
+ })).digest('hex'),
442
+ sample: {
443
+ participants: participants.size,
444
+ minimum: manifest.metrics.sample_size.minimum,
445
+ maximum: manifest.metrics.sample_size.maximum,
446
+ pass: samplePass
447
+ },
448
+ finalization: {
449
+ numerator: finalized,
450
+ denominator: eligible.length,
451
+ rate: finalizationRate,
452
+ pass: finalizationPass
453
+ },
454
+ nps: {
455
+ promoters,
456
+ passives,
457
+ detractors,
458
+ denominator: scores.length,
459
+ score: npsScore,
460
+ pass: npsPass
461
+ },
462
+ poi_hallucination: {
463
+ invalid_claims: invalidClaims,
464
+ locked_claims: lockedClaims,
465
+ rate: poiRate,
466
+ pass: poiPass
467
+ },
468
+ nightly: {
469
+ replayable_real_llm_runs: replayableRuns.length,
470
+ cost_usd: nightlyCost,
471
+ pass: nightlyPass
472
+ },
473
+ exclusions: exclusionCounts,
474
+ business_pass: businessPass,
475
+ business_pass_reason: businessPassReason
476
+ };
477
+ }
478
+ function parseJsonLines(contents) {
479
+ return contents.split(/\r?\n/).flatMap((line, index)=>{
480
+ if (line.trim() === '') return [];
481
+ try {
482
+ return [
483
+ JSON.parse(line)
484
+ ];
485
+ } catch (error) {
486
+ throw new Error(`cohort.jsonl line ${index + 1} is invalid JSON: ${error.message}`);
487
+ }
488
+ });
489
+ }
490
+ function loadFixture(path) {
491
+ const raw = object(JSON.parse(readFileSync(path, 'utf8')), 'fixture');
492
+ exactKeys(raw, 'fixture', [
493
+ 'schema_version',
494
+ 'manifest',
495
+ 'cohort',
496
+ 'nightly_runs'
497
+ ]);
498
+ literal(raw.schema_version, 'fixture.schema_version', [
499
+ 'gotry_m3_product_metrics_fixture_v1'
500
+ ]);
501
+ if (!Array.isArray(raw.cohort)) throw new Error('fixture.cohort must be an array');
502
+ if (!Array.isArray(raw.nightly_runs)) throw new Error('fixture.nightly_runs must be an array');
503
+ return {
504
+ manifest: parseManifest(raw.manifest),
505
+ cohort: raw.cohort.map(parseCohortRecord),
506
+ nightlyRuns: raw.nightly_runs.map(parseNightlyRun)
507
+ };
508
+ }
509
+ function loadEvidenceRoot(root) {
510
+ const manifest = parseManifest(JSON.parse(readFileSync(join(root, 'manifest.json'), 'utf8')));
511
+ const records = parseJsonLines(readFileSync(join(root, 'cohort.jsonl'), 'utf8'));
512
+ const cohort = [];
513
+ const nightlyRuns = [];
514
+ for (const record of records){
515
+ const schema = object(record, 'cohort.jsonl record').schema_version;
516
+ if (schema === 'gotry_m3_cohort_record_v1') cohort.push(parseCohortRecord(record, cohort.length));
517
+ else if (schema === 'gotry_m3_nightly_run_v1') nightlyRuns.push(parseNightlyRun(record, nightlyRuns.length));
518
+ else throw new Error(`cohort.jsonl contains unknown schema_version: ${String(schema)}`);
519
+ }
520
+ return {
521
+ manifest,
522
+ cohort,
523
+ nightlyRuns
524
+ };
525
+ }
526
+ function arg(name) {
527
+ const index = process.argv.indexOf(name);
528
+ return index >= 0 ? process.argv[index + 1] : undefined;
529
+ }
530
+ function renderMarkdown(summary) {
531
+ const percent = (value)=>value === null ? 'unavailable' : `${round(value * 100)}%`;
532
+ return [
533
+ `# M3 cohort 指标摘要(${summary.evidence_kind})`,
534
+ '',
535
+ `- 样本: ${summary.sample.participants} 人(pass=${summary.sample.pass};要求 ${summary.sample.minimum}..${summary.sample.maximum})`,
536
+ `- 定稿率: ${summary.finalization.numerator}/${summary.finalization.denominator} = ${percent(summary.finalization.rate)}(pass=${summary.finalization.pass})`,
537
+ `- NPS: ${summary.nps.score ?? 'unavailable'}(responses=${summary.nps.denominator};pass=${summary.nps.pass})`,
538
+ `- POI 幻觉率: ${summary.poi_hallucination.invalid_claims}/${summary.poi_hallucination.locked_claims} = ${percent(summary.poi_hallucination.rate)}(pass=${summary.poi_hallucination.pass})`,
539
+ `- Nightly real-LLM: ${summary.nightly.replayable_real_llm_runs} runs, $${summary.nightly.cost_usd}(pass=${summary.nightly.pass})`,
540
+ `- Business pass: ${summary.business_pass} — ${summary.business_pass_reason}`,
541
+ `- Evidence digest: ${summary.evidence_digest_sha256}`
542
+ ].join('\n');
543
+ }
544
+ function main() {
545
+ const fixturePath = arg('--fixture');
546
+ const evidenceRoot = arg('--evidence-root') ?? 'gotry-state/evidence/m3';
547
+ const loaded = fixturePath ? loadFixture(fixturePath) : loadEvidenceRoot(evidenceRoot);
548
+ const summary = scoreProductMetrics(loaded.manifest, loaded.cohort, loaded.nightlyRuns);
549
+ if (process.argv.includes('--write-summary')) {
550
+ if (fixturePath) throw new Error('--write-summary is forbidden for synthetic fixtures');
551
+ mkdirSync(evidenceRoot, {
552
+ recursive: true
553
+ });
554
+ writeFileSync(join(evidenceRoot, 'summary.json'), `${JSON.stringify(summary, null, 2)}\n`, 'utf8');
555
+ }
556
+ if ((arg('--format') ?? 'markdown') === 'json') console.log(JSON.stringify(summary));
557
+ else console.log(renderMarkdown(summary));
558
+ }
559
+ if (process.argv[1]?.endsWith('product-metrics.ts')) {
560
+ try {
561
+ main();
562
+ } catch (error) {
563
+ console.error(`M3 product metrics error: ${error.message}`);
564
+ process.exitCode = 1;
565
+ }
566
+ }
567
+
568
+
569
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/product-metrics.ts