@danceiny/gotry 0.0.1-rc.21 → 0.0.1-rc.24
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 +102 -194
- package/README.zh-CN.md +115 -210
- package/bin/gotry-backend.js +53 -0
- package/bin/gotry-bootstrap.js +466 -35
- package/bin/gotry-inner.js +255 -36
- package/bin/gotry-process-liveness.d.ts +27 -0
- package/bin/gotry-process-liveness.js +97 -0
- package/bin/gotry-runtime-resolution.d.ts +1 -1
- package/bin/gotry-runtime-resolution.js +2 -2
- package/client/client.js +174 -0
- package/cordis.gotry-patch.yml +70 -93
- package/dist/capabilities/agent-reach-deep.js +45 -122
- package/dist/capabilities/agent-reach.js +63 -148
- package/dist/capabilities/anything.js +89 -75
- package/dist/capabilities/artifacts.js +123 -104
- package/dist/capabilities/channel-health.js +65 -45
- package/dist/capabilities/channel-registry.js +108 -87
- package/dist/capabilities/doctor.js +357 -235
- package/dist/capabilities/effect.js +223 -309
- package/dist/capabilities/fact-log.js +28 -14
- package/dist/capabilities/flyai.js +245 -156
- package/dist/capabilities/ground-transfer.js +509 -0
- package/dist/capabilities/hbcli.js +144 -148
- package/dist/capabilities/hotelbyte-transaction.js +194 -0
- package/dist/capabilities/incident-log.js +131 -61
- package/dist/capabilities/model-override.js +30 -10
- package/dist/capabilities/opensky.js +54 -43
- package/dist/capabilities/resilience.js +50 -32
- package/dist/capabilities/session/action-cache.js +50 -44
- package/dist/capabilities/session/adapters/ctrip-flight.js +119 -62
- package/dist/capabilities/session/adapters/ctrip-hotel.js +125 -110
- package/dist/capabilities/session/adapters/dida-portal.js +206 -0
- package/dist/capabilities/session/adapters/meituan-local.js +35 -54
- package/dist/capabilities/session/adapters/rail-12306.js +169 -112
- package/dist/capabilities/session/benchmark.js +85 -149
- package/dist/capabilities/session/extension-bridge.js +320 -259
- package/dist/capabilities/session/extension-channel.js +57 -76
- package/dist/capabilities/session/extension-distribution.js +115 -119
- package/dist/capabilities/session/extract.js +31 -21
- package/dist/capabilities/session/golden-score.js +55 -33
- package/dist/capabilities/session/health-watch.js +146 -154
- package/dist/capabilities/session/read-guard.js +55 -47
- package/dist/capabilities/session/static-flight-golden.js +49 -36
- package/dist/capabilities/session/transport.js +54 -61
- package/dist/capabilities/session/wizard.js +63 -93
- package/dist/capabilities/session-consent.js +143 -47
- package/dist/capabilities/session-login.js +95 -126
- package/dist/capabilities/session-search.js +391 -253
- package/dist/capabilities/sponsor-plugin.js +341 -0
- package/dist/capabilities/visa-policy.js +87 -73
- package/dist/capabilities/weather.js +163 -235
- package/dist/scripts/action-cache-tests.js +24 -59
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +91 -142
- package/dist/scripts/agent-planning-turn-deadline-tests.js +80 -176
- package/dist/scripts/agent-reach-deep-tests.js +30 -40
- package/dist/scripts/agent-reach-tests.js +26 -18
- package/dist/scripts/agent-reach-wrapper-tests.js +46 -50
- package/dist/scripts/anything-tests.js +58 -62
- package/dist/scripts/artifact-client-contract-tests.js +69 -0
- package/dist/scripts/artifacts-capability-tests.js +235 -0
- package/dist/scripts/async-collect.js +26 -15
- package/dist/scripts/benchmark-environment-bridge-e2e.js +287 -684
- package/dist/scripts/benchmark-environment-bridge-tests.js +818 -3123
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +66 -279
- package/dist/scripts/booking-copilot-availability-policy-tests.js +153 -1257
- package/dist/scripts/booking-copilot-bin-proof-tests.js +22 -45
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +64 -76
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +69 -199
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +393 -759
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +33 -75
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +60 -110
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +28 -66
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +72 -271
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +73 -275
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +784 -3398
- package/dist/scripts/booking-copilot-server-proof-tests.js +63 -168
- package/dist/scripts/booking-copilot-startup-proof-tests.js +38 -124
- package/dist/scripts/booking-executor-tests.js +80 -0
- package/dist/scripts/booking-recovery-chain-tests.js +182 -0
- package/dist/scripts/booking-saga-tests.js +61 -123
- package/dist/scripts/booking-surface-contract-proof-tests.js +152 -1167
- package/dist/scripts/bootstrap-tests.js +1383 -128
- package/dist/scripts/build-changelog.js +89 -130
- package/dist/scripts/build-metrics-report.js +135 -143
- package/dist/scripts/changelog-tests.js +35 -82
- package/dist/scripts/channel-probe-tests.js +44 -116
- package/dist/scripts/channel-probe.js +84 -167
- package/dist/scripts/channel-registry-tests.js +97 -185
- package/dist/scripts/companion-tests.js +25 -70
- package/dist/scripts/dida-cookie-probe.js +87 -0
- package/dist/scripts/dida-sw-probe.js +114 -0
- package/dist/scripts/diff-test.js +17 -12
- package/dist/scripts/doctor-tests.js +428 -120
- package/dist/scripts/dsh-artifact-e2e.js +150 -0
- package/dist/scripts/dsh-artifact-web-e2e.js +443 -0
- package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
- package/dist/scripts/dsh-runtime-closure-tests.js +110 -153
- package/dist/scripts/dsh-runtime-closure.js +75 -63
- package/dist/scripts/dsh-session-v3-migration-proof.js +106 -0
- package/dist/scripts/dsh-subprocess-local-proof.js +149 -0
- package/dist/scripts/dsh-target-closure-proof.js +178 -0
- package/dist/scripts/effect-tests.js +165 -552
- package/dist/scripts/engine-run.js +1 -3
- package/dist/scripts/engine-tests.js +16 -10
- package/dist/scripts/evaluation-cadence-tests.js +69 -242
- package/dist/scripts/evaluation-contract-tests.js +193 -499
- package/dist/scripts/extension-distribution-cli.js +19 -15
- package/dist/scripts/extension-distribution-tests.js +98 -211
- package/dist/scripts/extension-tests.js +366 -389
- package/dist/scripts/fact-gate-tests.js +725 -263
- package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
- package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
- package/dist/scripts/flight-malformed-tests.js +159 -0
- package/dist/scripts/flyai-tests.js +325 -68
- package/dist/scripts/g5-guard-tests.js +131 -0
- package/dist/scripts/g5-guard.js +189 -0
- package/dist/scripts/gen-lunar-anchors.js +106 -0
- package/dist/scripts/gotry-backend-kernel-tests.js +79 -0
- package/dist/scripts/ground-transfer-tests.js +956 -0
- package/dist/scripts/hbcli-e2e-tests.js +71 -90
- package/dist/scripts/hbcli-live-optin-tests.js +96 -0
- package/dist/scripts/hbcli-release-contract-tests.js +122 -0
- package/dist/scripts/hbcli-tests.js +41 -115
- package/dist/scripts/health-watch-cli.js +42 -28
- package/dist/scripts/hotel-date-gate-tests.js +496 -0
- package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
- package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
- package/dist/scripts/i18n-tests.js +16 -9
- package/dist/scripts/incident-tests.js +281 -87
- package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
- package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
- package/dist/scripts/issue-271-liveness-tests.js +547 -0
- package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
- package/dist/scripts/issue-289-model-retry-tests.js +441 -0
- package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
- package/dist/scripts/issue-338-home-city-e2e.js +577 -0
- package/dist/scripts/issue343-real-entry-e2e.js +160 -0
- package/dist/scripts/journey-tests.js +22 -27
- package/dist/scripts/kernel-manifest-evidence.js +109 -0
- package/dist/scripts/kernel-manifest-gate.js +126 -0
- package/dist/scripts/kernel-manifest-tests.js +212 -0
- package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
- package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
- package/dist/scripts/kernel-manifest.js +369 -0
- package/dist/scripts/ledger-repair-apply-tests.js +316 -0
- package/dist/scripts/ledger-repair-plan-tests.js +375 -0
- package/dist/scripts/ledger-tests.js +325 -340
- package/dist/scripts/ledger-workflow-crash.js +10 -20
- package/dist/scripts/map-tools-vendor-package-proof.js +206 -134
- package/dist/scripts/memory-capture-tests.js +242 -61
- package/dist/scripts/memory-decay-tests.js +18 -52
- package/dist/scripts/memory-lifecycle-tests.js +657 -0
- package/dist/scripts/memory-lifecycle.js +196 -0
- package/dist/scripts/memory-metrics.js +24 -12
- package/dist/scripts/memory-value-report.js +474 -222
- package/dist/scripts/memory-value-tests.js +328 -0
- package/dist/scripts/metrics-report-tests.js +62 -199
- package/dist/scripts/model-override-e2e.js +66 -105
- package/dist/scripts/nightly-evidence-tests.js +64 -89
- package/dist/scripts/nightly-evidence.js +108 -111
- package/dist/scripts/nudge-digest.js +39 -28
- package/dist/scripts/onboarding-tests.js +63 -88
- package/dist/scripts/opensky-check.js +18 -13
- package/dist/scripts/opensky-tests.js +28 -28
- package/dist/scripts/persona-surface-guard-tests.js +37 -12
- package/dist/scripts/pnpm-dsh-closure-proof.js +2 -5
- package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
- package/dist/scripts/price-drift-tests.js +126 -227
- package/dist/scripts/price-drift-watch.js +209 -253
- package/dist/scripts/probe-poi-tests.js +52 -45
- package/dist/scripts/product-metrics.js +187 -273
- package/dist/scripts/publish-preverify.js +47 -41
- package/dist/scripts/realtime-pricing-tests.js +37 -76
- package/dist/scripts/replay-async.js +20 -18
- package/dist/scripts/replay-real.js +16 -18
- package/dist/scripts/replay.js +52 -82
- package/dist/scripts/session-attach-diagnose.js +16 -12
- package/dist/scripts/session-attach-poc.js +34 -37
- package/dist/scripts/session-benchmark.js +66 -147
- package/dist/scripts/session-dida-live-e2e.js +151 -0
- package/dist/scripts/session-extract-tests.js +14 -35
- package/dist/scripts/session-login.js +14 -13
- package/dist/scripts/session-tests.js +545 -583
- package/dist/scripts/sf-live-benchmark.js +92 -110
- package/dist/scripts/sf-live-cli-tests.js +2 -9
- package/dist/scripts/sf-soft-score-tests.js +19 -60
- package/dist/scripts/sf-summary-tests.js +149 -0
- package/dist/scripts/sf-summary.js +466 -84
- package/dist/scripts/skeleton-check.js +16 -14
- package/dist/scripts/skeleton-integration-test.js +11 -10
- package/dist/scripts/skills-contract-tests.js +51 -46
- package/dist/scripts/smoke-session-gate-tests.js +12 -20
- package/dist/scripts/smoke.js +541 -422
- package/dist/scripts/sponsor-reuse-tests.js +262 -0
- package/dist/scripts/state-cli-tests.js +226 -101
- package/dist/scripts/state-cli.js +549 -212
- package/dist/scripts/static-golden-tests.js +46 -180
- package/dist/scripts/time-eval-tests.js +210 -192
- package/dist/scripts/travel-timeline-tests.js +29 -93
- package/dist/scripts/turn-handoff-collect-tests.js +46 -93
- package/dist/scripts/turn-handoff-collect.js +71 -99
- package/dist/scripts/turn-policy-tests.js +28 -29
- package/dist/scripts/typed-contract-canary.js +77 -162
- package/dist/scripts/tz-resolver-sanity.js +50 -0
- package/dist/scripts/tz-resolver-tests.js +584 -0
- package/dist/scripts/unified-tests.js +23 -19
- package/dist/scripts/visa-policy-tests.js +31 -44
- package/dist/scripts/weather-tests.js +121 -543
- package/dist/scripts/wish-channel-gate-tests.js +29 -86
- package/dist/scripts/write-gate-tests.js +522 -0
- package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
- package/dist/scripts/z3-lifecycle-tests.js +170 -0
- package/dist/scripts/z3-race-repeat-tests.js +39 -0
- package/dist/scripts/z3-race-tests.js +18 -12
- package/dist/src/artifact-gate.js +652 -221
- package/dist/src/backend/kernel.js +91 -0
- package/dist/src/backend/modules/booking-copilot.js +55 -0
- package/dist/src/backend/modules/booking-executor.js +124 -0
- package/dist/src/backend/modules/session-search.js +194 -0
- package/dist/src/benchmark-agent-conformance.js +296 -262
- package/dist/src/benchmark-environment-bridge.js +298 -413
- package/dist/src/benchmark-headless-child-diagnostics.js +93 -100
- package/dist/src/benchmark-tool-isolation.js +94 -67
- package/dist/src/bookable-facts.js +299 -174
- package/dist/src/booking-saga.js +64 -120
- package/dist/src/booking-surface/availability-policy.js +443 -576
- package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
- package/dist/src/booking-surface/contracts.js +24 -80
- package/dist/src/booking-surface/dsh-planner.js +510 -281
- package/dist/src/booking-surface/dsh-plugin.js +17 -1
- package/dist/src/booking-surface/error-codes.js +82 -10
- package/dist/src/booking-surface/index.js +0 -3
- package/dist/src/booking-surface/profile.js +10 -54
- package/dist/src/booking-surface/recovery-chain.js +367 -0
- package/dist/src/booking-surface/runtime.js +765 -891
- package/dist/src/booking-surface/server.js +200 -251
- package/dist/src/booking-surface/startup.js +58 -54
- package/dist/src/booking-surface/validation.js +150 -218
- package/dist/src/bridge.js +20 -13
- package/dist/src/companions.js +48 -74
- package/dist/src/contracts.js +21 -43
- package/dist/src/dsh-llm.js +82 -120
- package/dist/src/engine.js +253 -239
- package/dist/src/evaluation-cadence.js +72 -209
- package/dist/src/evaluation-contracts.js +220 -843
- package/dist/src/flight-pack-adapter.js +54 -0
- package/dist/src/flight-pack-contract.js +2 -0
- package/dist/src/gotry-backend.js +70 -0
- package/dist/src/hotel-date-gate.js +118 -0
- package/dist/src/i18n.js +18 -7
- package/dist/src/index.js +1425 -1629
- package/dist/src/journey.js +132 -81
- package/dist/src/ledger-repair-apply.js +538 -0
- package/dist/src/ledger-repair-plan.js +463 -0
- package/dist/src/loop.js +257 -166
- package/dist/src/memory-capture.js +180 -32
- package/dist/src/memory-decay.js +30 -14
- package/dist/src/memory-lifecycle.js +1310 -0
- package/dist/src/memory-utility.js +27 -40
- package/dist/src/mock-llm.js +36 -61
- package/dist/src/model.js +81 -36
- package/dist/src/realtime-pricing.js +63 -72
- package/dist/src/slot-spec.js +84 -106
- package/dist/src/state-ledger.js +310 -471
- package/dist/src/subagent-job-id-guard.js +58 -0
- package/dist/src/time-anchor.js +231 -33
- package/dist/src/tool-packet.js +20 -8
- package/dist/src/travel-slots.js +58 -35
- package/dist/src/travel-timeline.js +44 -63
- package/dist/src/turn-deadline.js +121 -127
- package/dist/src/turn-policy.js +61 -89
- package/dist/src/tz-resolver.js +143 -0
- package/dist/src/unified.js +504 -381
- package/dist/src/wish-pool.js +39 -14
- package/dist/src/write-gate.js +495 -0
- package/dist/src/z3-shared.js +273 -7
- package/extension/README.md +4 -3
- package/extension/background.js +125 -14
- package/extension/content-bridge.js +23 -1
- package/extension/content-main.js +15 -4
- package/extension/manifest.json +16 -8
- package/package.json +251 -220
- package/ts/capabilities/agent-reach.ts +4 -4
- package/ts/capabilities/anything.ts +1 -1
- package/ts/capabilities/artifacts.ts +46 -9
- package/ts/capabilities/flyai.ts +156 -37
- package/ts/capabilities/hbcli.ts +1 -1
- package/ts/capabilities/incident-log.ts +88 -59
- package/ts/capabilities/session/adapters/ctrip-flight.ts +99 -36
- package/ts/capabilities/session/adapters/dida-portal.ts +250 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +123 -24
- package/ts/capabilities/session/extension-bridge.ts +226 -101
- package/ts/capabilities/session/extension-channel.ts +22 -9
- package/ts/capabilities/session/wizard.ts +2 -2
- package/ts/capabilities/session-consent.ts +98 -14
- package/ts/capabilities/session-login.ts +1 -0
- package/ts/capabilities/session-search.ts +315 -31
- package/ts/dsh-runtime/vendor/README.md +8 -4
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +7 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +5 -3
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +6 -6
- package/ts/package.json +236 -16
- package/ts/scripts/map-tools-vendor-package-proof.ts +152 -31
- package/ts/scripts/state-cli.ts +418 -51
- package/ts/src/artifact-gate.ts +561 -15
- package/ts/src/benchmark-environment-bridge.ts +37 -20
- package/ts/src/bookable-facts.ts +183 -7
- package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
- package/ts/src/booking-surface/dsh-planner.ts +240 -32
- package/ts/src/booking-surface/dsh-plugin.js +17 -1
- package/ts/src/booking-surface/error-codes.ts +75 -0
- package/ts/src/booking-surface/recovery-chain.ts +409 -0
- package/ts/src/booking-surface/server.ts +84 -51
- package/ts/src/booking-surface/validation.ts +2 -0
- package/ts/src/contracts.ts +12 -2
- package/ts/src/dsh-llm.ts +4 -8
- package/ts/src/index.ts +609 -90
- package/ts/src/loop.ts +116 -7
- package/ts/src/memory-capture.ts +227 -3
- package/ts/src/mock-llm.ts +4 -8
- package/ts/src/model.ts +88 -2
- package/ts/src/state-ledger.ts +47 -29
- package/ts/src/subagent-job-id-guard.ts +72 -0
- package/ts/src/time-anchor.ts +217 -13
- package/ts/src/unified.ts +316 -54
|
@@ -1,24 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* M3 真实种子 cohort 指标评分器(issue #22)。
|
|
3
|
+
*
|
|
4
|
+
* 私有证据默认位于 gotry-state/evidence/m3/:
|
|
5
|
+
* manifest.json 冻结样本窗口、纳排、分母、归因与阈值
|
|
6
|
+
* cohort.jsonl 脱敏 plan 记录与 nightly real-LLM 运行记录
|
|
7
|
+
* summary.json 可复跑的聚合结果(仅显式 --write-summary 时写入)
|
|
8
|
+
*
|
|
9
|
+
* 运行:
|
|
10
|
+
* npx tsx scripts/product-metrics.ts --evidence-root gotry-state/evidence/m3 --format json
|
|
11
|
+
* npx tsx scripts/product-metrics.ts --evidence-root gotry-state/evidence/m3 --write-summary
|
|
12
|
+
* npx tsx scripts/product-metrics.ts --fixture data/product-metrics-fixture.json --format json
|
|
13
|
+
*
|
|
14
|
+
* 纪律:
|
|
15
|
+
* - fixture 只验证 schema 与公式,永远不能得到 business_pass=true。
|
|
16
|
+
* - participant/plan/run 只接受 HMAC-SHA256 假名键;未知字段 fail-closed,
|
|
17
|
+
* 防止姓名、邮箱、电话、原始会话等进入证据面。
|
|
18
|
+
* - 分母为 0 时指标 unavailable(pass=false),不得把空样本算作 0% 幻觉。
|
|
19
|
+
*/
|
|
1
20
|
import { createHash } from 'node:crypto';
|
|
2
21
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
22
|
import { join } from 'node:path';
|
|
4
23
|
const FORMULAS = {
|
|
5
24
|
finalization: 'finalized_eligible_delivered_plans / eligible_delivered_plans',
|
|
6
25
|
nps: '100 * (promoters_9_10 - detractors_0_6) / valid_responses',
|
|
7
|
-
poi: 'audited_invalid_poi_claims / locked_audited_poi_claims'
|
|
26
|
+
poi: 'audited_invalid_poi_claims / locked_audited_poi_claims',
|
|
8
27
|
};
|
|
9
28
|
const EXCLUSION_CODES = [
|
|
10
29
|
'outside_locked_window',
|
|
11
30
|
'not_invited',
|
|
12
31
|
'consent_missing_or_withdrawn',
|
|
13
32
|
'test_or_staff',
|
|
14
|
-
'attribution_not_allowed'
|
|
33
|
+
'attribution_not_allowed',
|
|
15
34
|
];
|
|
16
35
|
const M3_ACCEPTANCE = {
|
|
17
36
|
sampleMinimum: 50,
|
|
18
37
|
sampleMaximum: 200,
|
|
19
38
|
finalizationMinimum: 0.4,
|
|
20
39
|
npsMinimum: 40,
|
|
21
|
-
poiExclusiveMaximum: 0.01
|
|
40
|
+
poiExclusiveMaximum: 0.01,
|
|
22
41
|
};
|
|
23
42
|
const HMAC_KEY = /^hmac-sha256:[0-9a-f]{64}$/;
|
|
24
43
|
const SHA256 = /^[0-9a-f]{64}$/;
|
|
@@ -30,155 +49,131 @@ function object(value, label) {
|
|
|
30
49
|
}
|
|
31
50
|
function exactKeys(value, label, keys) {
|
|
32
51
|
const expected = new Set(keys);
|
|
33
|
-
const unknown = Object.keys(value).filter(
|
|
34
|
-
const missing = keys.filter(
|
|
35
|
-
if (unknown.length > 0)
|
|
36
|
-
|
|
52
|
+
const unknown = Object.keys(value).filter(key => !expected.has(key));
|
|
53
|
+
const missing = keys.filter(key => !(key in value));
|
|
54
|
+
if (unknown.length > 0)
|
|
55
|
+
throw new Error(`${label} contains undeclared fields: ${unknown.join(', ')}`);
|
|
56
|
+
if (missing.length > 0)
|
|
57
|
+
throw new Error(`${label} missing fields: ${missing.join(', ')}`);
|
|
37
58
|
}
|
|
38
59
|
function string(value, label) {
|
|
39
|
-
if (typeof value !== 'string' || value.length === 0)
|
|
60
|
+
if (typeof value !== 'string' || value.length === 0)
|
|
61
|
+
throw new Error(`${label} must be a non-empty string`);
|
|
40
62
|
return value;
|
|
41
63
|
}
|
|
42
64
|
function boolean(value, label) {
|
|
43
|
-
if (typeof value !== 'boolean')
|
|
65
|
+
if (typeof value !== 'boolean')
|
|
66
|
+
throw new Error(`${label} must be boolean`);
|
|
44
67
|
return value;
|
|
45
68
|
}
|
|
46
69
|
function finiteNumber(value, label) {
|
|
47
|
-
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
70
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
71
|
+
throw new Error(`${label} must be a finite number`);
|
|
48
72
|
return value;
|
|
49
73
|
}
|
|
50
74
|
function integer(value, label) {
|
|
51
75
|
const result = finiteNumber(value, label);
|
|
52
|
-
if (!Number.isInteger(result))
|
|
76
|
+
if (!Number.isInteger(result))
|
|
77
|
+
throw new Error(`${label} must be an integer`);
|
|
53
78
|
return result;
|
|
54
79
|
}
|
|
55
80
|
function timestamp(value, label) {
|
|
56
81
|
const result = string(value, label);
|
|
57
|
-
if (!Number.isFinite(Date.parse(result)))
|
|
82
|
+
if (!Number.isFinite(Date.parse(result)))
|
|
83
|
+
throw new Error(`${label} must be an ISO timestamp`);
|
|
58
84
|
return result;
|
|
59
85
|
}
|
|
60
86
|
function nullableTimestamp(value, label) {
|
|
61
|
-
if (value === null)
|
|
87
|
+
if (value === null)
|
|
88
|
+
return null;
|
|
62
89
|
return timestamp(value, label);
|
|
63
90
|
}
|
|
64
91
|
function nullableNps(value, label) {
|
|
65
|
-
if (value === null)
|
|
92
|
+
if (value === null)
|
|
93
|
+
return null;
|
|
66
94
|
const result = integer(value, label);
|
|
67
|
-
if (result < 0 || result > 10)
|
|
95
|
+
if (result < 0 || result > 10)
|
|
96
|
+
throw new Error(`${label} must be an integer from 0 to 10`);
|
|
68
97
|
return result;
|
|
69
98
|
}
|
|
70
99
|
function hmacKey(value, label) {
|
|
71
100
|
const result = string(value, label);
|
|
72
|
-
if (!HMAC_KEY.test(result))
|
|
101
|
+
if (!HMAC_KEY.test(result))
|
|
102
|
+
throw new Error(`${label} must be an hmac-sha256 pseudonymous key`);
|
|
73
103
|
return result;
|
|
74
104
|
}
|
|
75
105
|
function sha256(value, label) {
|
|
76
106
|
const result = string(value, label);
|
|
77
|
-
if (!SHA256.test(result))
|
|
107
|
+
if (!SHA256.test(result))
|
|
108
|
+
throw new Error(`${label} must be a lowercase SHA-256 digest`);
|
|
78
109
|
return result;
|
|
79
110
|
}
|
|
80
111
|
function literal(value, label, allowed) {
|
|
81
112
|
const result = string(value, label);
|
|
82
|
-
if (!allowed.includes(result))
|
|
113
|
+
if (!allowed.includes(result))
|
|
114
|
+
throw new Error(`${label} must be one of ${allowed.join(', ')}`);
|
|
83
115
|
return result;
|
|
84
116
|
}
|
|
85
117
|
function literalArray(value, label, allowed) {
|
|
86
|
-
if (!Array.isArray(value) || value.length === 0)
|
|
87
|
-
|
|
88
|
-
|
|
118
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
119
|
+
throw new Error(`${label} must be a non-empty array`);
|
|
120
|
+
const result = value.map((item, index) => literal(item, `${label}[${index}]`, allowed));
|
|
121
|
+
if (new Set(result).size !== result.length)
|
|
122
|
+
throw new Error(`${label} must not contain duplicates`);
|
|
89
123
|
return result;
|
|
90
124
|
}
|
|
91
125
|
function exactStringArray(value, label, expected) {
|
|
92
|
-
if (!Array.isArray(value) || value.some(
|
|
126
|
+
if (!Array.isArray(value) || value.some(item => typeof item !== 'string')) {
|
|
93
127
|
throw new Error(`${label} must be a string array`);
|
|
94
128
|
}
|
|
95
|
-
const actual = [
|
|
96
|
-
|
|
97
|
-
].sort();
|
|
98
|
-
const wanted = [
|
|
99
|
-
...expected
|
|
100
|
-
].sort();
|
|
129
|
+
const actual = [...value].sort();
|
|
130
|
+
const wanted = [...expected].sort();
|
|
101
131
|
if (JSON.stringify(actual) !== JSON.stringify(wanted)) {
|
|
102
132
|
throw new Error(`${label} must freeze exactly: ${wanted.join(', ')}`);
|
|
103
133
|
}
|
|
104
134
|
return value;
|
|
105
135
|
}
|
|
106
136
|
function requiredTrue(value, label) {
|
|
107
|
-
if (value !== true)
|
|
137
|
+
if (value !== true)
|
|
138
|
+
throw new Error(`${label} must be true in schema v1`);
|
|
108
139
|
return true;
|
|
109
140
|
}
|
|
110
141
|
export function parseManifest(raw) {
|
|
111
142
|
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
|
-
]);
|
|
143
|
+
exactKeys(root, 'manifest', ['schema_version', 'evidence_kind', 'cohort_id', 'locked_at', 'window', 'eligibility', 'metrics', 'nightly']);
|
|
122
144
|
const window = object(root.window, 'manifest.window');
|
|
123
|
-
exactKeys(window, 'manifest.window', [
|
|
124
|
-
'start_at',
|
|
125
|
-
'end_at',
|
|
126
|
-
'timezone'
|
|
127
|
-
]);
|
|
145
|
+
exactKeys(window, 'manifest.window', ['start_at', 'end_at', 'timezone']);
|
|
128
146
|
const startAt = timestamp(window.start_at, 'manifest.window.start_at');
|
|
129
147
|
const endAt = timestamp(window.end_at, 'manifest.window.end_at');
|
|
130
|
-
if (Date.parse(startAt) > Date.parse(endAt))
|
|
148
|
+
if (Date.parse(startAt) > Date.parse(endAt))
|
|
149
|
+
throw new Error('manifest.window start_at must not be after end_at');
|
|
131
150
|
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
|
-
]);
|
|
151
|
+
exactKeys(eligibility, 'manifest.eligibility', ['sample_unit', 'requires_invitation', 'requires_consent', 'allowed_attribution', 'exclusion_codes']);
|
|
139
152
|
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
|
-
]);
|
|
153
|
+
exactKeys(metrics, 'manifest.metrics', ['sample_size', 'finalization_rate', 'nps', 'poi_hallucination_rate']);
|
|
146
154
|
const sampleSize = object(metrics.sample_size, 'manifest.metrics.sample_size');
|
|
147
|
-
exactKeys(sampleSize, 'manifest.metrics.sample_size', [
|
|
148
|
-
'minimum',
|
|
149
|
-
'maximum'
|
|
150
|
-
]);
|
|
155
|
+
exactKeys(sampleSize, 'manifest.metrics.sample_size', ['minimum', 'maximum']);
|
|
151
156
|
const finalization = object(metrics.finalization_rate, 'manifest.metrics.finalization_rate');
|
|
152
|
-
exactKeys(finalization, 'manifest.metrics.finalization_rate', [
|
|
153
|
-
'formula',
|
|
154
|
-
'minimum'
|
|
155
|
-
]);
|
|
157
|
+
exactKeys(finalization, 'manifest.metrics.finalization_rate', ['formula', 'minimum']);
|
|
156
158
|
const nps = object(metrics.nps, 'manifest.metrics.nps');
|
|
157
|
-
exactKeys(nps, 'manifest.metrics.nps', [
|
|
158
|
-
'formula',
|
|
159
|
-
'minimum'
|
|
160
|
-
]);
|
|
159
|
+
exactKeys(nps, 'manifest.metrics.nps', ['formula', 'minimum']);
|
|
161
160
|
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
|
-
]);
|
|
161
|
+
exactKeys(poi, 'manifest.metrics.poi_hallucination_rate', ['formula', 'exclusive_maximum']);
|
|
166
162
|
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
|
-
]);
|
|
163
|
+
exactKeys(nightly, 'manifest.nightly', ['requires_real_llm', 'requires_prompt_set_sha256', 'requires_output_sha256', 'requires_cost_usd']);
|
|
173
164
|
const minimum = integer(sampleSize.minimum, 'manifest.metrics.sample_size.minimum');
|
|
174
165
|
const maximum = integer(sampleSize.maximum, 'manifest.metrics.sample_size.maximum');
|
|
175
|
-
if (minimum < 1 || maximum < minimum)
|
|
166
|
+
if (minimum < 1 || maximum < minimum)
|
|
167
|
+
throw new Error('manifest sample size bounds are invalid');
|
|
176
168
|
const finalizationMinimum = finiteNumber(finalization.minimum, 'manifest.metrics.finalization_rate.minimum');
|
|
177
169
|
const npsMinimum = finiteNumber(nps.minimum, 'manifest.metrics.nps.minimum');
|
|
178
170
|
const poiMaximum = finiteNumber(poi.exclusive_maximum, 'manifest.metrics.poi_hallucination_rate.exclusive_maximum');
|
|
179
|
-
if (finalizationMinimum < 0 || finalizationMinimum > 1)
|
|
180
|
-
|
|
181
|
-
if (
|
|
171
|
+
if (finalizationMinimum < 0 || finalizationMinimum > 1)
|
|
172
|
+
throw new Error('finalization minimum must be within 0..1');
|
|
173
|
+
if (npsMinimum < -100 || npsMinimum > 100)
|
|
174
|
+
throw new Error('NPS minimum must be within -100..100');
|
|
175
|
+
if (poiMaximum <= 0 || poiMaximum > 1)
|
|
176
|
+
throw new Error('POI exclusive maximum must be within 0..1');
|
|
182
177
|
if (minimum !== M3_ACCEPTANCE.sampleMinimum || maximum !== M3_ACCEPTANCE.sampleMaximum) {
|
|
183
178
|
throw new Error(`M3 sample size thresholds are frozen at ${M3_ACCEPTANCE.sampleMinimum}..${M3_ACCEPTANCE.sampleMaximum}`);
|
|
184
179
|
}
|
|
@@ -192,87 +187,47 @@ export function parseManifest(raw) {
|
|
|
192
187
|
throw new Error(`M3 POI hallucination threshold is frozen at ${M3_ACCEPTANCE.poiExclusiveMaximum}`);
|
|
193
188
|
}
|
|
194
189
|
return {
|
|
195
|
-
schema_version: literal(root.schema_version, 'manifest.schema_version', [
|
|
196
|
-
|
|
197
|
-
]),
|
|
198
|
-
evidence_kind: literal(root.evidence_kind, 'manifest.evidence_kind', [
|
|
199
|
-
'real_seed_cohort',
|
|
200
|
-
'synthetic_fixture'
|
|
201
|
-
]),
|
|
190
|
+
schema_version: literal(root.schema_version, 'manifest.schema_version', ['gotry_m3_evidence_manifest_v1']),
|
|
191
|
+
evidence_kind: literal(root.evidence_kind, 'manifest.evidence_kind', ['real_seed_cohort', 'synthetic_fixture']),
|
|
202
192
|
cohort_id: string(root.cohort_id, 'manifest.cohort_id'),
|
|
203
193
|
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
|
-
},
|
|
194
|
+
window: { start_at: startAt, end_at: endAt, timezone: string(window.timezone, 'manifest.window.timezone') },
|
|
209
195
|
eligibility: {
|
|
210
|
-
sample_unit: literal(eligibility.sample_unit, 'manifest.eligibility.sample_unit', [
|
|
211
|
-
'unique_participant_with_eligible_delivered_plan'
|
|
212
|
-
]),
|
|
196
|
+
sample_unit: literal(eligibility.sample_unit, 'manifest.eligibility.sample_unit', ['unique_participant_with_eligible_delivered_plan']),
|
|
213
197
|
requires_invitation: requiredTrue(eligibility.requires_invitation, 'manifest.eligibility.requires_invitation'),
|
|
214
198
|
requires_consent: requiredTrue(eligibility.requires_consent, 'manifest.eligibility.requires_consent'),
|
|
215
|
-
allowed_attribution: literalArray(eligibility.allowed_attribution, 'manifest.eligibility.allowed_attribution', [
|
|
216
|
-
|
|
217
|
-
'gotry_assisted'
|
|
218
|
-
]),
|
|
219
|
-
exclusion_codes: exactStringArray(eligibility.exclusion_codes, 'manifest.eligibility.exclusion_codes', EXCLUSION_CODES)
|
|
199
|
+
allowed_attribution: literalArray(eligibility.allowed_attribution, 'manifest.eligibility.allowed_attribution', ['gotry_primary', 'gotry_assisted']),
|
|
200
|
+
exclusion_codes: exactStringArray(eligibility.exclusion_codes, 'manifest.eligibility.exclusion_codes', EXCLUSION_CODES),
|
|
220
201
|
},
|
|
221
202
|
metrics: {
|
|
222
|
-
sample_size: {
|
|
223
|
-
minimum,
|
|
224
|
-
maximum
|
|
225
|
-
},
|
|
203
|
+
sample_size: { minimum, maximum },
|
|
226
204
|
finalization_rate: {
|
|
227
|
-
formula: literal(finalization.formula, 'manifest.metrics.finalization_rate.formula', [
|
|
228
|
-
|
|
229
|
-
]),
|
|
230
|
-
minimum: finalizationMinimum
|
|
205
|
+
formula: literal(finalization.formula, 'manifest.metrics.finalization_rate.formula', [FORMULAS.finalization]),
|
|
206
|
+
minimum: finalizationMinimum,
|
|
231
207
|
},
|
|
232
208
|
nps: {
|
|
233
|
-
formula: literal(nps.formula, 'manifest.metrics.nps.formula', [
|
|
234
|
-
|
|
235
|
-
]),
|
|
236
|
-
minimum: npsMinimum
|
|
209
|
+
formula: literal(nps.formula, 'manifest.metrics.nps.formula', [FORMULAS.nps]),
|
|
210
|
+
minimum: npsMinimum,
|
|
237
211
|
},
|
|
238
212
|
poi_hallucination_rate: {
|
|
239
|
-
formula: literal(poi.formula, 'manifest.metrics.poi_hallucination_rate.formula', [
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
exclusive_maximum: poiMaximum
|
|
243
|
-
}
|
|
213
|
+
formula: literal(poi.formula, 'manifest.metrics.poi_hallucination_rate.formula', [FORMULAS.poi]),
|
|
214
|
+
exclusive_maximum: poiMaximum,
|
|
215
|
+
},
|
|
244
216
|
},
|
|
245
217
|
nightly: {
|
|
246
218
|
requires_real_llm: requiredTrue(nightly.requires_real_llm, 'manifest.nightly.requires_real_llm'),
|
|
247
219
|
requires_prompt_set_sha256: requiredTrue(nightly.requires_prompt_set_sha256, 'manifest.nightly.requires_prompt_set_sha256'),
|
|
248
220
|
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
|
-
}
|
|
221
|
+
requires_cost_usd: requiredTrue(nightly.requires_cost_usd, 'manifest.nightly.requires_cost_usd'),
|
|
222
|
+
},
|
|
251
223
|
};
|
|
252
224
|
}
|
|
253
225
|
export function parseCohortRecord(raw, index) {
|
|
254
226
|
const label = `cohort[${index}]`;
|
|
255
227
|
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
|
-
]);
|
|
228
|
+
exactKeys(root, label, ['schema_version', 'participant_key', 'plan_key', 'invited', 'consent', 'test_or_staff', 'attribution', 'delivered_at', 'finalized_at', 'nps_score', 'nps_recorded_at', 'poi_audit']);
|
|
270
229
|
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
|
-
]);
|
|
230
|
+
exactKeys(poi, `${label}.poi_audit`, ['locked_at', 'locked_claims', 'invalid_claims']);
|
|
276
231
|
const lockedClaims = integer(poi.locked_claims, `${label}.poi_audit.locked_claims`);
|
|
277
232
|
const invalidClaims = integer(poi.invalid_claims, `${label}.poi_audit.invalid_claims`);
|
|
278
233
|
if (lockedClaims < 0 || invalidClaims < 0 || invalidClaims > lockedClaims) {
|
|
@@ -286,7 +241,7 @@ export function parseCohortRecord(raw, index) {
|
|
|
286
241
|
if (finalizedAt !== null && Date.parse(finalizedAt) < Date.parse(deliveredAt)) {
|
|
287
242
|
throw new Error(`${label}.finalized_at must not be before delivered_at`);
|
|
288
243
|
}
|
|
289
|
-
if (npsScore === null !== (npsRecordedAt === null)) {
|
|
244
|
+
if ((npsScore === null) !== (npsRecordedAt === null)) {
|
|
290
245
|
throw new Error(`${label}.nps_score and nps_recorded_at must both be null or both be set`);
|
|
291
246
|
}
|
|
292
247
|
if (npsRecordedAt !== null && Date.parse(npsRecordedAt) < Date.parse(deliveredAt)) {
|
|
@@ -296,125 +251,112 @@ export function parseCohortRecord(raw, index) {
|
|
|
296
251
|
throw new Error(`${label}.poi_audit.locked_at must not be before delivered_at`);
|
|
297
252
|
}
|
|
298
253
|
return {
|
|
299
|
-
schema_version: literal(root.schema_version, `${label}.schema_version`, [
|
|
300
|
-
'gotry_m3_cohort_record_v1'
|
|
301
|
-
]),
|
|
254
|
+
schema_version: literal(root.schema_version, `${label}.schema_version`, ['gotry_m3_cohort_record_v1']),
|
|
302
255
|
participant_key: hmacKey(root.participant_key, `${label}.participant_key`),
|
|
303
256
|
plan_key: hmacKey(root.plan_key, `${label}.plan_key`),
|
|
304
257
|
invited: boolean(root.invited, `${label}.invited`),
|
|
305
258
|
consent: boolean(root.consent, `${label}.consent`),
|
|
306
259
|
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
|
-
]),
|
|
260
|
+
attribution: literal(root.attribution, `${label}.attribution`, ['gotry_primary', 'gotry_assisted']),
|
|
311
261
|
delivered_at: deliveredAt,
|
|
312
262
|
finalized_at: finalizedAt,
|
|
313
263
|
nps_score: npsScore,
|
|
314
264
|
nps_recorded_at: npsRecordedAt,
|
|
315
|
-
poi_audit: {
|
|
316
|
-
locked_at: poiLockedAt,
|
|
317
|
-
locked_claims: lockedClaims,
|
|
318
|
-
invalid_claims: invalidClaims
|
|
319
|
-
}
|
|
265
|
+
poi_audit: { locked_at: poiLockedAt, locked_claims: lockedClaims, invalid_claims: invalidClaims },
|
|
320
266
|
};
|
|
321
267
|
}
|
|
322
268
|
export function parseNightlyRun(raw, index) {
|
|
323
269
|
const label = `nightly_runs[${index}]`;
|
|
324
270
|
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
|
-
]);
|
|
271
|
+
exactKeys(root, label, ['schema_version', 'run_key', 'executed_at', 'real_llm', 'prompt_set_sha256', 'output_sha256', 'cost_usd']);
|
|
334
272
|
const costUsd = finiteNumber(root.cost_usd, `${label}.cost_usd`);
|
|
335
|
-
if (costUsd < 0)
|
|
273
|
+
if (costUsd < 0)
|
|
274
|
+
throw new Error(`${label}.cost_usd must be non-negative`);
|
|
336
275
|
return {
|
|
337
|
-
schema_version: literal(root.schema_version, `${label}.schema_version`, [
|
|
338
|
-
'gotry_m3_nightly_run_v1'
|
|
339
|
-
]),
|
|
276
|
+
schema_version: literal(root.schema_version, `${label}.schema_version`, ['gotry_m3_nightly_run_v1']),
|
|
340
277
|
run_key: hmacKey(root.run_key, `${label}.run_key`),
|
|
341
278
|
executed_at: timestamp(root.executed_at, `${label}.executed_at`),
|
|
342
279
|
real_llm: boolean(root.real_llm, `${label}.real_llm`),
|
|
343
280
|
prompt_set_sha256: sha256(root.prompt_set_sha256, `${label}.prompt_set_sha256`),
|
|
344
281
|
output_sha256: sha256(root.output_sha256, `${label}.output_sha256`),
|
|
345
|
-
cost_usd: costUsd
|
|
282
|
+
cost_usd: costUsd,
|
|
346
283
|
};
|
|
347
284
|
}
|
|
348
285
|
function round(value) {
|
|
349
286
|
return Number(value.toFixed(6));
|
|
350
287
|
}
|
|
351
288
|
function canonical(value) {
|
|
352
|
-
if (Array.isArray(value))
|
|
289
|
+
if (Array.isArray(value))
|
|
290
|
+
return `[${value.map(canonical).join(',')}]`;
|
|
353
291
|
if (typeof value === 'object' && value !== null) {
|
|
354
292
|
const record = value;
|
|
355
|
-
return `{${Object.keys(record).sort().map(
|
|
293
|
+
return `{${Object.keys(record).sort().map(key => `${JSON.stringify(key)}:${canonical(record[key])}`).join(',')}}`;
|
|
356
294
|
}
|
|
357
295
|
return JSON.stringify(value);
|
|
358
296
|
}
|
|
359
297
|
function exclusionsFor(manifest, record) {
|
|
360
298
|
const result = [];
|
|
361
299
|
const delivered = Date.parse(record.delivered_at);
|
|
362
|
-
if (delivered < Date.parse(manifest.window.start_at) || delivered > Date.parse(manifest.window.end_at))
|
|
363
|
-
|
|
364
|
-
if (!record.
|
|
365
|
-
|
|
366
|
-
if (!
|
|
300
|
+
if (delivered < Date.parse(manifest.window.start_at) || delivered > Date.parse(manifest.window.end_at))
|
|
301
|
+
result.push('outside_locked_window');
|
|
302
|
+
if (!record.invited)
|
|
303
|
+
result.push('not_invited');
|
|
304
|
+
if (!record.consent)
|
|
305
|
+
result.push('consent_missing_or_withdrawn');
|
|
306
|
+
if (record.test_or_staff)
|
|
307
|
+
result.push('test_or_staff');
|
|
308
|
+
if (!manifest.eligibility.allowed_attribution.includes(record.attribution))
|
|
309
|
+
result.push('attribution_not_allowed');
|
|
367
310
|
return result;
|
|
368
311
|
}
|
|
369
312
|
export function scoreProductMetrics(manifest, cohort, nightlyRuns) {
|
|
370
313
|
const planKeys = new Set();
|
|
371
314
|
const runKeys = new Set();
|
|
372
|
-
for (const record of cohort){
|
|
373
|
-
if (planKeys.has(record.plan_key))
|
|
315
|
+
for (const record of cohort) {
|
|
316
|
+
if (planKeys.has(record.plan_key))
|
|
317
|
+
throw new Error(`duplicate plan_key: ${record.plan_key}`);
|
|
374
318
|
planKeys.add(record.plan_key);
|
|
375
319
|
}
|
|
376
|
-
for (const run of nightlyRuns){
|
|
377
|
-
if (runKeys.has(run.run_key))
|
|
320
|
+
for (const run of nightlyRuns) {
|
|
321
|
+
if (runKeys.has(run.run_key))
|
|
322
|
+
throw new Error(`duplicate run_key: ${run.run_key}`);
|
|
378
323
|
runKeys.add(run.run_key);
|
|
379
324
|
}
|
|
380
|
-
const exclusionCounts = Object.fromEntries(EXCLUSION_CODES.map(
|
|
381
|
-
|
|
382
|
-
0
|
|
383
|
-
]));
|
|
384
|
-
const eligible = cohort.filter((record)=>{
|
|
325
|
+
const exclusionCounts = Object.fromEntries(EXCLUSION_CODES.map(code => [code, 0]));
|
|
326
|
+
const eligible = cohort.filter(record => {
|
|
385
327
|
const exclusions = exclusionsFor(manifest, record);
|
|
386
|
-
for (const code of exclusions)
|
|
328
|
+
for (const code of exclusions)
|
|
329
|
+
exclusionCounts[code] += 1;
|
|
387
330
|
return exclusions.length === 0;
|
|
388
331
|
});
|
|
389
|
-
const participants = new Set(eligible.map(
|
|
332
|
+
const participants = new Set(eligible.map(record => record.participant_key));
|
|
390
333
|
const windowEnd = Date.parse(manifest.window.end_at);
|
|
391
|
-
const finalized = eligible.filter(
|
|
334
|
+
const finalized = eligible.filter(record => record.finalized_at !== null && Date.parse(record.finalized_at) <= windowEnd).length;
|
|
392
335
|
const finalizationRate = eligible.length === 0 ? null : round(finalized / eligible.length);
|
|
393
336
|
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)
|
|
337
|
+
for (const record of eligible) {
|
|
338
|
+
if (record.nps_score === null || record.nps_recorded_at === null || Date.parse(record.nps_recorded_at) > windowEnd)
|
|
339
|
+
continue;
|
|
396
340
|
if (npsByParticipant.has(record.participant_key)) {
|
|
397
341
|
throw new Error(`participant has more than one NPS response in the locked window: ${record.participant_key}`);
|
|
398
342
|
}
|
|
399
343
|
npsByParticipant.set(record.participant_key, record.nps_score);
|
|
400
344
|
}
|
|
401
|
-
const scores = [
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
const promoters = scores.filter((score)=>score >= 9).length;
|
|
405
|
-
const detractors = scores.filter((score)=>score <= 6).length;
|
|
345
|
+
const scores = [...npsByParticipant.values()];
|
|
346
|
+
const promoters = scores.filter(score => score >= 9).length;
|
|
347
|
+
const detractors = scores.filter(score => score <= 6).length;
|
|
406
348
|
const passives = scores.length - promoters - detractors;
|
|
407
349
|
const npsScore = scores.length === 0 ? null : round(100 * (promoters - detractors) / scores.length);
|
|
408
|
-
const lockedAudits = eligible.filter(
|
|
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);
|
|
350
|
+
const lockedAudits = eligible.filter(record => Date.parse(record.poi_audit.locked_at) <= windowEnd);
|
|
351
|
+
const lockedClaims = lockedAudits.reduce((sum, record) => sum + record.poi_audit.locked_claims, 0);
|
|
352
|
+
const invalidClaims = lockedAudits.reduce((sum, record) => sum + record.poi_audit.invalid_claims, 0);
|
|
411
353
|
const poiRate = lockedClaims === 0 ? null : round(invalidClaims / lockedClaims);
|
|
412
354
|
const windowStart = Date.parse(manifest.window.start_at);
|
|
413
|
-
const replayableRuns = nightlyRuns.filter(
|
|
355
|
+
const replayableRuns = nightlyRuns.filter(run => {
|
|
414
356
|
const executedAt = Date.parse(run.executed_at);
|
|
415
357
|
return run.real_llm && executedAt >= windowStart && executedAt <= windowEnd;
|
|
416
358
|
});
|
|
417
|
-
const nightlyCost = round(replayableRuns.reduce((sum, run)=>sum + run.cost_usd, 0));
|
|
359
|
+
const nightlyCost = round(replayableRuns.reduce((sum, run) => sum + run.cost_usd, 0));
|
|
418
360
|
const samplePass = participants.size >= manifest.metrics.sample_size.minimum && participants.size <= manifest.metrics.sample_size.maximum;
|
|
419
361
|
const finalizationPass = finalizationRate !== null && finalizationRate >= manifest.metrics.finalization_rate.minimum;
|
|
420
362
|
const npsPass = npsScore !== null && npsScore >= manifest.metrics.nps.minimum;
|
|
@@ -427,83 +369,57 @@ export function scoreProductMetrics(manifest, cohort, nightlyRuns) {
|
|
|
427
369
|
!finalizationPass && 'finalization_rate',
|
|
428
370
|
!npsPass && 'nps',
|
|
429
371
|
!poiPass && 'poi_hallucination_rate',
|
|
430
|
-
!nightlyPass && 'nightly_real_llm'
|
|
372
|
+
!nightlyPass && 'nightly_real_llm',
|
|
431
373
|
].filter(Boolean);
|
|
432
|
-
const businessPassReason = manifest.evidence_kind === 'synthetic_fixture'
|
|
374
|
+
const businessPassReason = manifest.evidence_kind === 'synthetic_fixture'
|
|
375
|
+
? 'evidence_kind=synthetic_fixture cannot prove business pass'
|
|
376
|
+
: businessPass
|
|
377
|
+
? 'all locked M3 acceptance thresholds passed on real_seed_cohort evidence'
|
|
378
|
+
: `real_seed_cohort acceptance gaps: ${failedChecks.join(', ')}`;
|
|
433
379
|
return {
|
|
434
380
|
schema_version: 'gotry_m3_product_metrics_summary_v1',
|
|
435
381
|
evidence_kind: manifest.evidence_kind,
|
|
436
382
|
cohort_id: manifest.cohort_id,
|
|
437
|
-
evidence_digest_sha256: createHash('sha256').update(canonical({
|
|
438
|
-
manifest,
|
|
439
|
-
cohort,
|
|
440
|
-
nightlyRuns
|
|
441
|
-
})).digest('hex'),
|
|
383
|
+
evidence_digest_sha256: createHash('sha256').update(canonical({ manifest, cohort, nightlyRuns })).digest('hex'),
|
|
442
384
|
sample: {
|
|
443
385
|
participants: participants.size,
|
|
444
386
|
minimum: manifest.metrics.sample_size.minimum,
|
|
445
387
|
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
|
|
388
|
+
pass: samplePass,
|
|
472
389
|
},
|
|
390
|
+
finalization: { numerator: finalized, denominator: eligible.length, rate: finalizationRate, pass: finalizationPass },
|
|
391
|
+
nps: { promoters, passives, detractors, denominator: scores.length, score: npsScore, pass: npsPass },
|
|
392
|
+
poi_hallucination: { invalid_claims: invalidClaims, locked_claims: lockedClaims, rate: poiRate, pass: poiPass },
|
|
393
|
+
nightly: { replayable_real_llm_runs: replayableRuns.length, cost_usd: nightlyCost, pass: nightlyPass },
|
|
473
394
|
exclusions: exclusionCounts,
|
|
474
395
|
business_pass: businessPass,
|
|
475
|
-
business_pass_reason: businessPassReason
|
|
396
|
+
business_pass_reason: businessPassReason,
|
|
476
397
|
};
|
|
477
398
|
}
|
|
478
399
|
function parseJsonLines(contents) {
|
|
479
|
-
return contents.split(/\r?\n/).flatMap((line, index)=>{
|
|
480
|
-
if (line.trim() === '')
|
|
400
|
+
return contents.split(/\r?\n/).flatMap((line, index) => {
|
|
401
|
+
if (line.trim() === '')
|
|
402
|
+
return [];
|
|
481
403
|
try {
|
|
482
|
-
return [
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
} catch (error) {
|
|
404
|
+
return [JSON.parse(line)];
|
|
405
|
+
}
|
|
406
|
+
catch (error) {
|
|
486
407
|
throw new Error(`cohort.jsonl line ${index + 1} is invalid JSON: ${error.message}`);
|
|
487
408
|
}
|
|
488
409
|
});
|
|
489
410
|
}
|
|
490
411
|
function loadFixture(path) {
|
|
491
412
|
const raw = object(JSON.parse(readFileSync(path, 'utf8')), 'fixture');
|
|
492
|
-
exactKeys(raw, 'fixture', [
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
'cohort'
|
|
496
|
-
|
|
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');
|
|
413
|
+
exactKeys(raw, 'fixture', ['schema_version', 'manifest', 'cohort', 'nightly_runs']);
|
|
414
|
+
literal(raw.schema_version, 'fixture.schema_version', ['gotry_m3_product_metrics_fixture_v1']);
|
|
415
|
+
if (!Array.isArray(raw.cohort))
|
|
416
|
+
throw new Error('fixture.cohort must be an array');
|
|
417
|
+
if (!Array.isArray(raw.nightly_runs))
|
|
418
|
+
throw new Error('fixture.nightly_runs must be an array');
|
|
503
419
|
return {
|
|
504
420
|
manifest: parseManifest(raw.manifest),
|
|
505
421
|
cohort: raw.cohort.map(parseCohortRecord),
|
|
506
|
-
nightlyRuns: raw.nightly_runs.map(parseNightlyRun)
|
|
422
|
+
nightlyRuns: raw.nightly_runs.map(parseNightlyRun),
|
|
507
423
|
};
|
|
508
424
|
}
|
|
509
425
|
function loadEvidenceRoot(root) {
|
|
@@ -511,24 +427,23 @@ function loadEvidenceRoot(root) {
|
|
|
511
427
|
const records = parseJsonLines(readFileSync(join(root, 'cohort.jsonl'), 'utf8'));
|
|
512
428
|
const cohort = [];
|
|
513
429
|
const nightlyRuns = [];
|
|
514
|
-
for (const record of records){
|
|
430
|
+
for (const record of records) {
|
|
515
431
|
const schema = object(record, 'cohort.jsonl record').schema_version;
|
|
516
|
-
if (schema === 'gotry_m3_cohort_record_v1')
|
|
517
|
-
|
|
518
|
-
else
|
|
432
|
+
if (schema === 'gotry_m3_cohort_record_v1')
|
|
433
|
+
cohort.push(parseCohortRecord(record, cohort.length));
|
|
434
|
+
else if (schema === 'gotry_m3_nightly_run_v1')
|
|
435
|
+
nightlyRuns.push(parseNightlyRun(record, nightlyRuns.length));
|
|
436
|
+
else
|
|
437
|
+
throw new Error(`cohort.jsonl contains unknown schema_version: ${String(schema)}`);
|
|
519
438
|
}
|
|
520
|
-
return {
|
|
521
|
-
manifest,
|
|
522
|
-
cohort,
|
|
523
|
-
nightlyRuns
|
|
524
|
-
};
|
|
439
|
+
return { manifest, cohort, nightlyRuns };
|
|
525
440
|
}
|
|
526
441
|
function arg(name) {
|
|
527
442
|
const index = process.argv.indexOf(name);
|
|
528
443
|
return index >= 0 ? process.argv[index + 1] : undefined;
|
|
529
444
|
}
|
|
530
445
|
function renderMarkdown(summary) {
|
|
531
|
-
const percent = (value)=>value === null ? 'unavailable' : `${round(value * 100)}%`;
|
|
446
|
+
const percent = (value) => value === null ? 'unavailable' : `${round(value * 100)}%`;
|
|
532
447
|
return [
|
|
533
448
|
`# M3 cohort 指标摘要(${summary.evidence_kind})`,
|
|
534
449
|
'',
|
|
@@ -538,7 +453,7 @@ function renderMarkdown(summary) {
|
|
|
538
453
|
`- POI 幻觉率: ${summary.poi_hallucination.invalid_claims}/${summary.poi_hallucination.locked_claims} = ${percent(summary.poi_hallucination.rate)}(pass=${summary.poi_hallucination.pass})`,
|
|
539
454
|
`- Nightly real-LLM: ${summary.nightly.replayable_real_llm_runs} runs, $${summary.nightly.cost_usd}(pass=${summary.nightly.pass})`,
|
|
540
455
|
`- Business pass: ${summary.business_pass} — ${summary.business_pass_reason}`,
|
|
541
|
-
`- Evidence digest: ${summary.evidence_digest_sha256}
|
|
456
|
+
`- Evidence digest: ${summary.evidence_digest_sha256}`,
|
|
542
457
|
].join('\n');
|
|
543
458
|
}
|
|
544
459
|
function main() {
|
|
@@ -547,23 +462,22 @@ function main() {
|
|
|
547
462
|
const loaded = fixturePath ? loadFixture(fixturePath) : loadEvidenceRoot(evidenceRoot);
|
|
548
463
|
const summary = scoreProductMetrics(loaded.manifest, loaded.cohort, loaded.nightlyRuns);
|
|
549
464
|
if (process.argv.includes('--write-summary')) {
|
|
550
|
-
if (fixturePath)
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
});
|
|
465
|
+
if (fixturePath)
|
|
466
|
+
throw new Error('--write-summary is forbidden for synthetic fixtures');
|
|
467
|
+
mkdirSync(evidenceRoot, { recursive: true });
|
|
554
468
|
writeFileSync(join(evidenceRoot, 'summary.json'), `${JSON.stringify(summary, null, 2)}\n`, 'utf8');
|
|
555
469
|
}
|
|
556
|
-
if ((arg('--format') ?? 'markdown') === 'json')
|
|
557
|
-
|
|
470
|
+
if ((arg('--format') ?? 'markdown') === 'json')
|
|
471
|
+
console.log(JSON.stringify(summary));
|
|
472
|
+
else
|
|
473
|
+
console.log(renderMarkdown(summary));
|
|
558
474
|
}
|
|
559
475
|
if (process.argv[1]?.endsWith('product-metrics.ts')) {
|
|
560
476
|
try {
|
|
561
477
|
main();
|
|
562
|
-
}
|
|
478
|
+
}
|
|
479
|
+
catch (error) {
|
|
563
480
|
console.error(`M3 product metrics error: ${error.message}`);
|
|
564
481
|
process.exitCode = 1;
|
|
565
482
|
}
|
|
566
483
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
//# sourceURL=ts/scripts/product-metrics.ts
|