@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,3 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #138 指标面板第一切片:只读聚合呈现面 v1(ADR-11 质量层,工程面交付)。
|
|
3
|
+
*
|
|
4
|
+
* 聚合既有落盘侧车(不新增任何数据源)→ 单一 markdown 报告:
|
|
5
|
+
* - 事实闸 verdict 分布 + blocked 牊(<stateRoot>/gotry-state/bookable-facts.jsonl)
|
|
6
|
+
* - 通道健康 down/cooldown('ok' 恢复事件 latest-wins 超越;<stateRoot>/gotry-state/channel-health.jsonl,30 天窗,
|
|
7
|
+
* 与 doctor 持久面 readLatestChannelEvents 同口径)
|
|
8
|
+
* - 事故面(<stateRoot>/gotry-state/incidents.jsonl,--days 窗,默认 7)
|
|
9
|
+
* - 桥延迟 p50/p95/max + >500ms 违约计数(<stateRoot>/gotry-state/bridge-latency.jsonl;
|
|
10
|
+
* >500ms 是架构 §11 复审节奏的触发锚点,这里给出可见性)
|
|
11
|
+
* - 账本存在性(gotry-state.db 只 stat:openDb 建连即可能写 kv,指标面不打开 SQLite)
|
|
12
|
+
* - doctor 报告存在性 + 评测三层入口指针(运行结果由 run-all-tests.sh/CI 承载,v1 不重跑评测)
|
|
13
|
+
*
|
|
14
|
+
* 纪律:全程只读(缺文件=空节;坏行跳过,与 incidents/fact-log 同纪律);
|
|
15
|
+
* 默认写 stdout,仅 --out 指定路径时落盘;零新依赖零 LLM;
|
|
16
|
+
* 工程面交付,不构成 M3 Exit 证据(#138 口径,Exit 证据归 #22)。
|
|
17
|
+
*
|
|
18
|
+
* 运行:npx tsx ts/scripts/build-metrics-report.ts [--state-root <dir>] [--out <file>] [--days <N>]
|
|
19
|
+
*/
|
|
1
20
|
import { existsSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
2
21
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
3
22
|
const BRIDGE_LATENCY_BUDGET_MS = 500;
|
|
@@ -6,107 +25,104 @@ function stateFile(stateRoot, name) {
|
|
|
6
25
|
const root = stateRoot === '.' ? process.cwd() : stateRoot;
|
|
7
26
|
return join(root, 'gotry-state', name);
|
|
8
27
|
}
|
|
28
|
+
/** 坏行跳过的 JSONL 读取(与 incidents/fact-log/channel-health 同纪律) */
|
|
9
29
|
function readJsonl(path) {
|
|
10
|
-
if (!existsSync(path))
|
|
30
|
+
if (!existsSync(path))
|
|
31
|
+
return [];
|
|
11
32
|
try {
|
|
12
33
|
const rows = [];
|
|
13
|
-
for (const line of readFileSync(path, 'utf-8').split('\n')){
|
|
34
|
+
for (const line of readFileSync(path, 'utf-8').split('\n')) {
|
|
14
35
|
const t = line.trim();
|
|
15
|
-
if (!t)
|
|
36
|
+
if (!t)
|
|
37
|
+
continue;
|
|
16
38
|
try {
|
|
17
39
|
rows.push(JSON.parse(t));
|
|
18
|
-
}
|
|
40
|
+
}
|
|
41
|
+
catch { /* 单行损坏不拖垮聚合 */ }
|
|
19
42
|
}
|
|
20
43
|
return rows;
|
|
21
|
-
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
22
46
|
return [];
|
|
23
47
|
}
|
|
24
48
|
}
|
|
25
49
|
function bounded(value, max = 120) {
|
|
26
|
-
if (!value)
|
|
50
|
+
if (!value)
|
|
51
|
+
return undefined;
|
|
27
52
|
return value.length <= max ? value : `${value.slice(0, max - 1)}…`;
|
|
28
53
|
}
|
|
29
|
-
const BLOCKED_VERDICTS = new Set([
|
|
30
|
-
|
|
31
|
-
'needs-setup'
|
|
32
|
-
]);
|
|
54
|
+
const BLOCKED_VERDICTS = new Set(['error', 'needs-setup']);
|
|
55
|
+
/** 同 fact_id 取最新(与 loadFactRegistry 的 dedupe 同语义;行序后者胜) */
|
|
33
56
|
export function aggregateFacts(rows) {
|
|
34
57
|
const latest = new Map();
|
|
35
58
|
let lines = 0;
|
|
36
|
-
for (const raw of rows){
|
|
37
|
-
const row = typeof raw === 'object' && raw !== null ? raw : {};
|
|
59
|
+
for (const raw of rows) {
|
|
60
|
+
const row = (typeof raw === 'object' && raw !== null ? raw : {});
|
|
38
61
|
lines += 1;
|
|
39
62
|
const id = typeof row.fact_id === 'string' ? row.fact_id : '';
|
|
40
|
-
if (!id)
|
|
63
|
+
if (!id)
|
|
64
|
+
continue;
|
|
41
65
|
latest.set(id, row);
|
|
42
66
|
}
|
|
43
67
|
const byKind = {};
|
|
44
|
-
for (const row of latest.values()){
|
|
68
|
+
for (const row of latest.values()) {
|
|
45
69
|
const kind = typeof row.kind === 'string' && row.kind ? row.kind : 'unknown';
|
|
46
70
|
const verdict = typeof row.verdict === 'string' && row.verdict ? row.verdict : 'n/a';
|
|
47
|
-
const slot = byKind[kind] ??= {
|
|
48
|
-
total: 0,
|
|
49
|
-
verdicts: {},
|
|
50
|
-
blocked: 0
|
|
51
|
-
};
|
|
71
|
+
const slot = (byKind[kind] ??= { total: 0, verdicts: {}, blocked: 0 });
|
|
52
72
|
slot.total += 1;
|
|
53
73
|
slot.verdicts[verdict] = (slot.verdicts[verdict] ?? 0) + 1;
|
|
54
|
-
if (BLOCKED_VERDICTS.has(verdict))
|
|
74
|
+
if (BLOCKED_VERDICTS.has(verdict))
|
|
75
|
+
slot.blocked += 1;
|
|
55
76
|
}
|
|
56
|
-
return {
|
|
57
|
-
lines,
|
|
58
|
-
deduped: latest.size,
|
|
59
|
-
byKind
|
|
60
|
-
};
|
|
77
|
+
return { lines, deduped: latest.size, byKind };
|
|
61
78
|
}
|
|
62
79
|
export function aggregateChannels(rows, opts = {}) {
|
|
63
80
|
const nowMs = (opts.now ?? new Date()).getTime();
|
|
64
81
|
const cutoff = nowMs - (opts.windowDays ?? CHANNEL_WINDOW_DAYS) * 86_400_000;
|
|
65
82
|
const counts = new Map();
|
|
66
83
|
const newest = new Map();
|
|
84
|
+
// ok 超越(外部事件接缝第 1 段):通道最新事件若为 'ok'(本地探针恢复),
|
|
85
|
+
// 该通道从「当前 down/cooldown」展示面退出——down 计数(历史频率)保留。
|
|
67
86
|
const lastState = new Map();
|
|
68
|
-
for (const raw of rows){
|
|
69
|
-
const row = typeof raw === 'object' && raw !== null ? raw : {};
|
|
70
|
-
if (typeof row.channel !== 'string' || !row.channel)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
'cooldown',
|
|
74
|
-
'ok'
|
|
75
|
-
].includes(row.state)) {
|
|
87
|
+
for (const raw of rows) {
|
|
88
|
+
const row = (typeof raw === 'object' && raw !== null ? raw : {});
|
|
89
|
+
if (typeof row.channel !== 'string' || !row.channel)
|
|
90
|
+
continue;
|
|
91
|
+
if (typeof row.state === 'string' && ['down', 'cooldown', 'ok'].includes(row.state)) {
|
|
76
92
|
lastState.set(row.channel, row.state);
|
|
77
93
|
}
|
|
78
94
|
}
|
|
79
|
-
for (const raw of rows){
|
|
80
|
-
const row = typeof raw === 'object' && raw !== null ? raw : {};
|
|
81
|
-
if (typeof row.channel !== 'string' || !row.channel)
|
|
82
|
-
|
|
83
|
-
if (
|
|
95
|
+
for (const raw of rows) {
|
|
96
|
+
const row = (typeof raw === 'object' && raw !== null ? raw : {});
|
|
97
|
+
if (typeof row.channel !== 'string' || !row.channel)
|
|
98
|
+
continue;
|
|
99
|
+
if (row.state !== 'down' && row.state !== 'cooldown')
|
|
100
|
+
continue;
|
|
101
|
+
if (lastState.get(row.channel) === 'ok')
|
|
102
|
+
continue;
|
|
84
103
|
const at = typeof row.at === 'string' ? row.at : '';
|
|
85
104
|
const atMs = Date.parse(at);
|
|
86
|
-
if (Number.isFinite(atMs) && atMs < cutoff)
|
|
105
|
+
if (Number.isFinite(atMs) && atMs < cutoff)
|
|
106
|
+
continue;
|
|
87
107
|
counts.set(row.channel, (counts.get(row.channel) ?? 0) + 1);
|
|
88
108
|
const atMsSafe = Number.isFinite(atMs) ? atMs : 0;
|
|
89
109
|
const prev = newest.get(row.channel);
|
|
90
|
-
if (prev && prev.atMs >= atMsSafe)
|
|
110
|
+
if (prev && prev.atMs >= atMsSafe)
|
|
111
|
+
continue;
|
|
91
112
|
newest.set(row.channel, {
|
|
92
113
|
channel: row.channel,
|
|
93
114
|
state: row.state,
|
|
94
115
|
reason: typeof row.reason === 'string' ? row.reason : undefined,
|
|
95
116
|
at: at || undefined,
|
|
96
117
|
events: 0,
|
|
97
|
-
atMs: atMsSafe
|
|
118
|
+
atMs: atMsSafe,
|
|
98
119
|
});
|
|
99
120
|
}
|
|
100
|
-
const latest = [
|
|
101
|
-
...
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}));
|
|
106
|
-
return {
|
|
107
|
-
windowDays: opts.windowDays ?? CHANNEL_WINDOW_DAYS,
|
|
108
|
-
latest
|
|
109
|
-
};
|
|
121
|
+
const latest = [...newest.values()].map(({ atMs: _atMs, ...row }) => ({
|
|
122
|
+
...row,
|
|
123
|
+
events: counts.get(row.channel) ?? 0,
|
|
124
|
+
}));
|
|
125
|
+
return { windowDays: opts.windowDays ?? CHANNEL_WINDOW_DAYS, latest };
|
|
110
126
|
}
|
|
111
127
|
export function aggregateIncidents(rows, opts = {}) {
|
|
112
128
|
const nowMs = (opts.now ?? new Date()).getTime();
|
|
@@ -115,17 +131,16 @@ export function aggregateIncidents(rows, opts = {}) {
|
|
|
115
131
|
const byKind = {};
|
|
116
132
|
const sources = new Map();
|
|
117
133
|
let total = 0;
|
|
118
|
-
for (const raw of rows){
|
|
119
|
-
const row = typeof raw === 'object' && raw !== null ? raw : {};
|
|
134
|
+
for (const raw of rows) {
|
|
135
|
+
const row = (typeof raw === 'object' && raw !== null ? raw : {});
|
|
120
136
|
const ts = typeof row.ts === 'string' ? row.ts : '';
|
|
121
137
|
const atMs = Date.parse(ts);
|
|
122
|
-
if (Number.isFinite(atMs) && atMs < cutoff)
|
|
138
|
+
if (Number.isFinite(atMs) && atMs < cutoff)
|
|
139
|
+
continue;
|
|
123
140
|
const kind = typeof row.kind === 'string' && row.kind ? row.kind : 'unknown';
|
|
124
|
-
const slot = byKind[kind] ??= {
|
|
125
|
-
count: 0
|
|
126
|
-
};
|
|
141
|
+
const slot = (byKind[kind] ??= { count: 0 });
|
|
127
142
|
slot.count += 1;
|
|
128
|
-
if (!slot.lastTs || Number.isFinite(atMs) && atMs > Date.parse(slot.lastTs)) {
|
|
143
|
+
if (!slot.lastTs || (Number.isFinite(atMs) && atMs > Date.parse(slot.lastTs))) {
|
|
129
144
|
slot.lastTs = ts || slot.lastTs;
|
|
130
145
|
slot.lastMessage = bounded(typeof row.message === 'string' ? row.message : undefined);
|
|
131
146
|
}
|
|
@@ -133,69 +148,51 @@ export function aggregateIncidents(rows, opts = {}) {
|
|
|
133
148
|
sources.set(source, (sources.get(source) ?? 0) + 1);
|
|
134
149
|
total += 1;
|
|
135
150
|
}
|
|
136
|
-
const topSources = [
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}));
|
|
142
|
-
return {
|
|
143
|
-
windowDays,
|
|
144
|
-
totalInWindow: total,
|
|
145
|
-
byKind,
|
|
146
|
-
topSources
|
|
147
|
-
};
|
|
151
|
+
const topSources = [...sources.entries()]
|
|
152
|
+
.sort((a, b) => b[1] - a[1])
|
|
153
|
+
.slice(0, 5)
|
|
154
|
+
.map(([source, count]) => ({ source, count }));
|
|
155
|
+
return { windowDays, totalInWindow: total, byKind, topSources };
|
|
148
156
|
}
|
|
149
157
|
export function aggregateLatency(rows, opts = {}) {
|
|
150
158
|
const nowMs = (opts.now ?? new Date()).getTime();
|
|
151
159
|
const cutoff = nowMs - (opts.windowDays ?? 7) * 86_400_000;
|
|
152
160
|
const values = [];
|
|
153
161
|
const kinds = new Map();
|
|
154
|
-
for (const raw of rows){
|
|
155
|
-
const row = typeof raw === 'object' && raw !== null ? raw : {};
|
|
162
|
+
for (const raw of rows) {
|
|
163
|
+
const row = (typeof raw === 'object' && raw !== null ? raw : {});
|
|
156
164
|
const ts = typeof row.ts === 'string' ? row.ts : '';
|
|
157
165
|
const atMs = Date.parse(ts);
|
|
158
|
-
if (Number.isFinite(atMs) && atMs < cutoff)
|
|
166
|
+
if (Number.isFinite(atMs) && atMs < cutoff)
|
|
167
|
+
continue;
|
|
159
168
|
const ms = typeof row.latencyMs === 'number' ? row.latencyMs : Number.NaN;
|
|
160
|
-
if (!Number.isFinite(ms))
|
|
169
|
+
if (!Number.isFinite(ms))
|
|
170
|
+
continue;
|
|
161
171
|
values.push(ms);
|
|
162
172
|
const kind = typeof row.kind === 'string' && row.kind ? row.kind : 'unknown';
|
|
163
173
|
kinds.set(kind, (kinds.get(kind) ?? 0) + 1);
|
|
164
174
|
}
|
|
165
|
-
const sorted = [
|
|
166
|
-
|
|
167
|
-
].sort((a, b)=>a - b);
|
|
168
|
-
const pick = (q)=>sorted.length === 0 ? 0 : sorted[Math.min(sorted.length - 1, Math.ceil(q * sorted.length) - 1)];
|
|
175
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
176
|
+
const pick = (q) => (sorted.length === 0 ? 0 : sorted[Math.min(sorted.length - 1, Math.ceil(q * sorted.length) - 1)]);
|
|
169
177
|
return {
|
|
170
178
|
count: sorted.length,
|
|
171
179
|
p50: pick(0.5),
|
|
172
180
|
p95: pick(0.95),
|
|
173
181
|
max: sorted.length === 0 ? 0 : sorted[sorted.length - 1],
|
|
174
|
-
overBudget: sorted.filter((ms)=>ms > BRIDGE_LATENCY_BUDGET_MS).length,
|
|
182
|
+
overBudget: sorted.filter((ms) => ms > BRIDGE_LATENCY_BUDGET_MS).length,
|
|
175
183
|
budgetMs: BRIDGE_LATENCY_BUDGET_MS,
|
|
176
|
-
topKinds: [
|
|
177
|
-
...kinds.entries()
|
|
178
|
-
].sort((a, b)=>b[1] - a[1]).slice(0, 5).map(([kind, count])=>({
|
|
179
|
-
kind,
|
|
180
|
-
count
|
|
181
|
-
}))
|
|
184
|
+
topKinds: [...kinds.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([kind, count]) => ({ kind, count })),
|
|
182
185
|
};
|
|
183
186
|
}
|
|
184
187
|
function presenceOf(path) {
|
|
185
188
|
try {
|
|
186
|
-
if (!existsSync(path))
|
|
187
|
-
exists: false
|
|
188
|
-
};
|
|
189
|
+
if (!existsSync(path))
|
|
190
|
+
return { exists: false };
|
|
189
191
|
const st = statSync(path);
|
|
190
|
-
return {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
};
|
|
195
|
-
} catch {
|
|
196
|
-
return {
|
|
197
|
-
exists: false
|
|
198
|
-
};
|
|
192
|
+
return { exists: true, bytes: st.size, mtime: st.mtime.toISOString() };
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return { exists: false };
|
|
199
196
|
}
|
|
200
197
|
}
|
|
201
198
|
export async function collectMetrics(stateRoot, opts = {}) {
|
|
@@ -204,27 +201,19 @@ export async function collectMetrics(stateRoot, opts = {}) {
|
|
|
204
201
|
generatedAt: (opts.now ?? new Date()).toISOString(),
|
|
205
202
|
stateRoot: root,
|
|
206
203
|
facts: aggregateFacts(readJsonl(stateFile(stateRoot, 'bookable-facts.jsonl'))),
|
|
207
|
-
channels: aggregateChannels(readJsonl(stateFile(stateRoot, 'channel-health.jsonl')), {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}),
|
|
211
|
-
incidents: aggregateIncidents(readJsonl(stateFile(stateRoot, 'incidents.jsonl')), {
|
|
212
|
-
now: opts.now,
|
|
213
|
-
windowDays: opts.days ?? 7
|
|
214
|
-
}),
|
|
215
|
-
latency: aggregateLatency(readJsonl(stateFile(stateRoot, 'bridge-latency.jsonl')), {
|
|
216
|
-
now: opts.now,
|
|
217
|
-
windowDays: opts.days ?? 7
|
|
218
|
-
}),
|
|
204
|
+
channels: aggregateChannels(readJsonl(stateFile(stateRoot, 'channel-health.jsonl')), { now: opts.now, windowDays: CHANNEL_WINDOW_DAYS }),
|
|
205
|
+
incidents: aggregateIncidents(readJsonl(stateFile(stateRoot, 'incidents.jsonl')), { now: opts.now, windowDays: opts.days ?? 7 }),
|
|
206
|
+
latency: aggregateLatency(readJsonl(stateFile(stateRoot, 'bridge-latency.jsonl')), { now: opts.now, windowDays: opts.days ?? 7 }),
|
|
219
207
|
ledger: presenceOf(stateFile(stateRoot, 'gotry-state.db')),
|
|
220
|
-
doctorReport: presenceOf(stateFile(stateRoot, 'doctor-report.md'))
|
|
208
|
+
doctorReport: presenceOf(stateFile(stateRoot, 'doctor-report.md')),
|
|
221
209
|
};
|
|
222
210
|
}
|
|
223
211
|
function fmtRate(blocked, total) {
|
|
224
|
-
return total === 0 ? '—' : `${blocked}/${total}(${(blocked / total * 100).toFixed(0)}%)`;
|
|
212
|
+
return total === 0 ? '—' : `${blocked}/${total}(${((blocked / total) * 100).toFixed(0)}%)`;
|
|
225
213
|
}
|
|
226
214
|
function fmtBytes(bytes) {
|
|
227
|
-
if (bytes === undefined)
|
|
215
|
+
if (bytes === undefined)
|
|
216
|
+
return '—';
|
|
228
217
|
return bytes >= 1_048_576 ? `${(bytes / 1_048_576).toFixed(1)}MB` : `${bytes}B`;
|
|
229
218
|
}
|
|
230
219
|
export function renderMetricsReport(s) {
|
|
@@ -240,11 +229,12 @@ export function renderMetricsReport(s) {
|
|
|
240
229
|
const kinds = Object.entries(s.facts.byKind);
|
|
241
230
|
if (kinds.length === 0) {
|
|
242
231
|
L.push('(空:无事实落盘)');
|
|
243
|
-
}
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
244
234
|
L.push('| kind | 去重后 | verdict 分布 | blocked(error+needs-setup) |');
|
|
245
235
|
L.push('|---|---|---|---|');
|
|
246
|
-
for (const [kind, st] of kinds){
|
|
247
|
-
const dist = Object.entries(st.verdicts).map(([v, n])
|
|
236
|
+
for (const [kind, st] of kinds) {
|
|
237
|
+
const dist = Object.entries(st.verdicts).map(([v, n]) => `${v}:${n}`).join(' / ');
|
|
248
238
|
L.push(`| ${kind} | ${st.total} | ${dist} | ${fmtRate(st.blocked, st.total)} |`);
|
|
249
239
|
}
|
|
250
240
|
L.push('');
|
|
@@ -255,10 +245,11 @@ export function renderMetricsReport(s) {
|
|
|
255
245
|
L.push('');
|
|
256
246
|
if (s.channels.latest.length === 0) {
|
|
257
247
|
L.push('(空:窗口内无 down/cooldown 事件)');
|
|
258
|
-
}
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
259
250
|
L.push('| 通道 | 最新状态 | 原因 | 时间 | 窗口事件 |');
|
|
260
251
|
L.push('|---|---|---|---|---|');
|
|
261
|
-
for (const c of s.channels.latest){
|
|
252
|
+
for (const c of s.channels.latest) {
|
|
262
253
|
L.push(`| ${c.channel} | ${c.state} | ${bounded(c.reason ?? '', 60) ?? '—'} | ${c.at ?? '—'} | ${c.events} |`);
|
|
263
254
|
}
|
|
264
255
|
}
|
|
@@ -268,13 +259,14 @@ export function renderMetricsReport(s) {
|
|
|
268
259
|
const incidentKinds = Object.entries(s.incidents.byKind);
|
|
269
260
|
if (incidentKinds.length === 0) {
|
|
270
261
|
L.push('(空:窗口内无事故)');
|
|
271
|
-
}
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
272
264
|
L.push('| kind | 次数 | 最近一次 |');
|
|
273
265
|
L.push('|---|---|---|');
|
|
274
|
-
for (const [kind, st] of incidentKinds){
|
|
266
|
+
for (const [kind, st] of incidentKinds) {
|
|
275
267
|
L.push(`| ${kind} | ${st.count} | ${st.lastTs ?? '—'}${st.lastMessage ? ` · ${st.lastMessage}` : ''} |`);
|
|
276
268
|
}
|
|
277
|
-
const src = s.incidents.topSources.map((t)
|
|
269
|
+
const src = s.incidents.topSources.map((t) => `${t.source}×${t.count}`).join(', ');
|
|
278
270
|
if (src) {
|
|
279
271
|
L.push('');
|
|
280
272
|
L.push(`Top 来源:${src}`);
|
|
@@ -285,16 +277,18 @@ export function renderMetricsReport(s) {
|
|
|
285
277
|
L.push('');
|
|
286
278
|
if (s.latency.count === 0) {
|
|
287
279
|
L.push('(空:窗口内无延迟记录)');
|
|
288
|
-
}
|
|
289
|
-
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
const kinds = s.latency.topKinds.map((t) => `${t.kind}×${t.count}`).join(', ');
|
|
290
283
|
L.push(`样本 ${s.latency.count}:p50=${s.latency.p50}ms · p95=${s.latency.p95}ms · max=${s.latency.max}ms · 超预算(>${s.latency.budgetMs}ms)= **${s.latency.overBudget}**`);
|
|
291
|
-
if (kinds)
|
|
284
|
+
if (kinds)
|
|
285
|
+
L.push(`按 kind:${kinds}`);
|
|
292
286
|
}
|
|
293
287
|
L.push('');
|
|
294
288
|
L.push('## 账本与体检报告');
|
|
295
289
|
L.push('');
|
|
296
|
-
L.push(`- 账本 gotry-state.db:${s.ledger.exists ? `存在(${fmtBytes(s.ledger.bytes)},更新 ${s.ledger.mtime};明细经 \`npx gotry state\` 查看,指标面不打开 SQLite)` : '不存在(该 stateRoot 无账本)'}`);
|
|
297
|
-
L.push(`- doctor 报告 doctor-report.md:${s.doctorReport.exists ? `存在(更新 ${s.doctorReport.mtime})` : '不存在(跑 `npx gotry doctor` 生成)'}`);
|
|
290
|
+
L.push(`- 账本 gotry-state.db:${s.ledger.exists ? `存在(${fmtBytes(s.ledger.bytes)},更新 ${s.ledger.mtime};明细经 \`npx @danceiny/gotry state\` 查看,指标面不打开 SQLite)` : '不存在(该 stateRoot 无账本)'}`);
|
|
291
|
+
L.push(`- doctor 报告 doctor-report.md:${s.doctorReport.exists ? `存在(更新 ${s.doctorReport.mtime})` : '不存在(跑 `npx @danceiny/gotry doctor` 生成)'}`);
|
|
298
292
|
L.push('');
|
|
299
293
|
L.push('## 评测三层入口(ADR-11;运行结果由 run-all-tests.sh / CI 承载,v1 不在此重跑)');
|
|
300
294
|
L.push('');
|
|
@@ -303,37 +297,35 @@ export function renderMetricsReport(s) {
|
|
|
303
297
|
L.push('');
|
|
304
298
|
return L.join('\n');
|
|
305
299
|
}
|
|
300
|
+
// ---- CLI ---------------------------------------------------------------------
|
|
306
301
|
export function parseArgs(argv) {
|
|
307
302
|
let stateRoot = '.';
|
|
308
303
|
let out;
|
|
309
304
|
let days = 7;
|
|
310
|
-
for(let i = 0; i < argv.length; i += 1){
|
|
305
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
311
306
|
const a = argv[i];
|
|
312
|
-
if (a === '--state-root')
|
|
313
|
-
|
|
314
|
-
else if (a === '--
|
|
307
|
+
if (a === '--state-root')
|
|
308
|
+
stateRoot = argv[++i] ?? stateRoot;
|
|
309
|
+
else if (a === '--out')
|
|
310
|
+
out = argv[++i];
|
|
311
|
+
else if (a === '--days')
|
|
312
|
+
days = Math.max(1, Number.parseInt(argv[++i] ?? '7', 10) || 7);
|
|
315
313
|
}
|
|
316
|
-
return {
|
|
317
|
-
stateRoot: isAbsolute(stateRoot) ? stateRoot : resolve(stateRoot),
|
|
318
|
-
out,
|
|
319
|
-
days
|
|
320
|
-
};
|
|
314
|
+
return { stateRoot: isAbsolute(stateRoot) ? stateRoot : resolve(stateRoot), out, days };
|
|
321
315
|
}
|
|
322
|
-
export async function main(argv) {
|
|
316
|
+
export async function main(argv, injectedNow) {
|
|
323
317
|
const { stateRoot, out, days } = parseArgs(argv);
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
});
|
|
318
|
+
const now = injectedNow ?? new Date();
|
|
319
|
+
const snapshot = await collectMetrics(stateRoot, { days, now });
|
|
327
320
|
const markdown = renderMetricsReport(snapshot);
|
|
328
321
|
if (out) {
|
|
329
322
|
writeFileSync(out, markdown + '\n', 'utf-8');
|
|
330
323
|
process.stdout.write(`指标报告已写 ${out}(stateRoot 只读未动)\n`);
|
|
331
|
-
}
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
332
326
|
process.stdout.write(markdown + '\n');
|
|
333
327
|
}
|
|
334
328
|
}
|
|
335
329
|
const invokedDirectly = process.argv[1] && import.meta.url === new URL(`file://${resolve(process.argv[1])}`).href;
|
|
336
|
-
if (invokedDirectly)
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
//# sourceURL=ts/scripts/build-metrics-report.ts
|
|
330
|
+
if (invokedDirectly)
|
|
331
|
+
await main(process.argv.slice(2));
|
|
@@ -1,10 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CHANGELOG 生成器合同测试(run-all §42;2026-08-30 owner 拍板补 changelog 机制)。
|
|
3
|
+
* 纯函数、零网络、零 git 调用:
|
|
4
|
+
* - Conventional Commits 解析:feat/fix/perf/refactor/docs/test/build/ci/chore/revert + scope + ! + PR 引用
|
|
5
|
+
* - 不符合 Conventional 的 commit → other 桶(不丢历史)
|
|
6
|
+
* - groupCommits 按 Keep a Changelog 显示顺序输出
|
|
7
|
+
* - renderMarkdownEntry 节结构(Added/Changed/Fixed/...)
|
|
8
|
+
* - prependEntry:不覆盖 CHANGELOG.md 历史段,只在顶部插入新版本段
|
|
9
|
+
* - splitChangelog:preamble/history 分隔正确
|
|
10
|
+
*/
|
|
1
11
|
import assert from 'node:assert/strict';
|
|
2
|
-
import { buildEntry, groupCommits, parseCommit, prependEntry, renderMarkdownEntry, splitChangelog } from './build-changelog.js';
|
|
12
|
+
import { buildEntry, groupCommits, parseCommit, prependEntry, renderMarkdownEntry, splitChangelog, } from './build-changelog.js';
|
|
3
13
|
let passed = 0;
|
|
4
14
|
function ok(label) {
|
|
5
15
|
passed += 1;
|
|
6
16
|
console.log(` ok ${label}`);
|
|
7
17
|
}
|
|
18
|
+
// ---- 1. parseCommit:Conventional 完整形态 ----
|
|
8
19
|
const sample = parseCommit('feat(cost): 价表 provider-aware v2 + 价格漂移监测 (#68)');
|
|
9
20
|
assert.ok(sample);
|
|
10
21
|
assert.equal(sample.type, 'feat');
|
|
@@ -12,12 +23,14 @@ assert.equal(sample.scope, 'cost');
|
|
|
12
23
|
assert.equal(sample.subject, '价表 provider-aware v2 + 价格漂移监测');
|
|
13
24
|
assert.equal(sample.prRef, '68');
|
|
14
25
|
ok('parseCommit feat(scope)! + #PR');
|
|
26
|
+
// ---- 2. parseCommit:不带 scope ----
|
|
15
27
|
const noScope = parseCommit('fix: 修复登录页可见性');
|
|
16
28
|
assert.equal(noScope.type, 'fix');
|
|
17
29
|
assert.equal(noScope.scope, null);
|
|
18
30
|
assert.equal(noScope.subject, '修复登录页可见性');
|
|
19
31
|
assert.equal(noScope.prRef, null);
|
|
20
32
|
ok('parseCommit fix 无 scope');
|
|
33
|
+
// ---- 3. parseCommit:不符合 Conventional → other 桶,保留原文 ----
|
|
21
34
|
const other = parseCommit('docs: README 更新');
|
|
22
35
|
assert.equal(other.type, 'docs');
|
|
23
36
|
const legacy = parseCommit('feat(issue #21 P3.6): 会话扩展 onboarding UX 闭环(点点点 + 装完零重跑)');
|
|
@@ -27,53 +40,18 @@ const historical = parseCommit('Merge branch main into feat/x');
|
|
|
27
40
|
assert.equal(historical.type, 'other');
|
|
28
41
|
assert.equal(historical.subject, 'Merge branch main into feat/x');
|
|
29
42
|
ok('parseCommit 历史 commit + 中文 subject + 无 #PR 兼容');
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
'refactor',
|
|
33
|
-
'docs',
|
|
34
|
-
'test',
|
|
35
|
-
'build',
|
|
36
|
-
'ci',
|
|
37
|
-
'chore',
|
|
38
|
-
'revert'
|
|
39
|
-
]){
|
|
43
|
+
// ---- 4. parseCommit:perf/refactor/docs/test/build/ci/chore/revert 全覆盖 ----
|
|
44
|
+
for (const t of ['perf', 'refactor', 'docs', 'test', 'build', 'ci', 'chore', 'revert']) {
|
|
40
45
|
const p = parseCommit(`${t}: 一些改动`);
|
|
41
46
|
assert.equal(p.type, t);
|
|
42
47
|
}
|
|
43
48
|
ok('parseCommit 全 9 类型支持');
|
|
49
|
+
// ---- 5. groupCommits:Keep a Changelog 显示顺序 ----
|
|
44
50
|
const commits = [
|
|
45
|
-
{
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
subject: 'fix bug',
|
|
50
|
-
prRef: '1',
|
|
51
|
-
raw: ''
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
sha: 'bbb',
|
|
55
|
-
type: 'feat',
|
|
56
|
-
scope: null,
|
|
57
|
-
subject: 'add feature',
|
|
58
|
-
prRef: '2',
|
|
59
|
-
raw: ''
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
sha: 'ccc',
|
|
63
|
-
type: 'docs',
|
|
64
|
-
scope: null,
|
|
65
|
-
subject: 'update docs',
|
|
66
|
-
prRef: null,
|
|
67
|
-
raw: ''
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
sha: 'ddd',
|
|
71
|
-
type: 'other',
|
|
72
|
-
scope: null,
|
|
73
|
-
subject: 'random commit',
|
|
74
|
-
prRef: null,
|
|
75
|
-
raw: ''
|
|
76
|
-
}
|
|
51
|
+
{ sha: 'aaa', type: 'fix', scope: null, subject: 'fix bug', prRef: '1', raw: '' },
|
|
52
|
+
{ sha: 'bbb', type: 'feat', scope: null, subject: 'add feature', prRef: '2', raw: '' },
|
|
53
|
+
{ sha: 'ccc', type: 'docs', scope: null, subject: 'update docs', prRef: null, raw: '' },
|
|
54
|
+
{ sha: 'ddd', type: 'other', scope: null, subject: 'random commit', prRef: null, raw: '' },
|
|
77
55
|
];
|
|
78
56
|
const grouped = groupCommits(commits);
|
|
79
57
|
assert.equal(grouped.length, 4);
|
|
@@ -82,36 +60,13 @@ assert.equal(grouped[1].type, 'fix');
|
|
|
82
60
|
assert.equal(grouped[2].type, 'docs');
|
|
83
61
|
assert.equal(grouped[3].type, 'other');
|
|
84
62
|
ok('groupCommits Keep a Changelog 显示顺序(Added → Fixed → Documentation → Other)');
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
{
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
scope: null,
|
|
93
|
-
subject: 'add a brand new feature with substantial description text',
|
|
94
|
-
prRef: '2',
|
|
95
|
-
raw: ''
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
sha: 'bbb',
|
|
99
|
-
type: 'fix',
|
|
100
|
-
scope: null,
|
|
101
|
-
subject: 'fix a critical bug in the rendering path of the parser',
|
|
102
|
-
prRef: '1',
|
|
103
|
-
raw: ''
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
sha: 'ccc',
|
|
107
|
-
type: 'docs',
|
|
108
|
-
scope: null,
|
|
109
|
-
subject: 'update the documentation for the new auth module',
|
|
110
|
-
prRef: null,
|
|
111
|
-
raw: ''
|
|
112
|
-
}
|
|
113
|
-
]
|
|
114
|
-
});
|
|
63
|
+
// ---- 6. renderMarkdownEntry:节结构 + PR 引用 + sha 后缀 ----
|
|
64
|
+
// 用较长 subject 避开 truncate 逻辑(短 subject 仍可能触发 breaker,断言用「包含」而非「完全等」)
|
|
65
|
+
const entry = buildEntry({ version: '0.0.1-rc.16', date: '2026-08-30', commits: [
|
|
66
|
+
{ sha: 'aaa', type: 'feat', scope: null, subject: 'add a brand new feature with substantial description text', prRef: '2', raw: '' },
|
|
67
|
+
{ sha: 'bbb', type: 'fix', scope: null, subject: 'fix a critical bug in the rendering path of the parser', prRef: '1', raw: '' },
|
|
68
|
+
{ sha: 'ccc', type: 'docs', scope: null, subject: 'update the documentation for the new auth module', prRef: null, raw: '' },
|
|
69
|
+
] });
|
|
115
70
|
const md = renderMarkdownEntry(entry);
|
|
116
71
|
assert.match(md, /^## \[0\.0\.1-rc\.16\] - 2026-08-30/);
|
|
117
72
|
assert.match(md, /### Added/);
|
|
@@ -119,8 +74,9 @@ assert.match(md, /### Fixed/);
|
|
|
119
74
|
assert.match(md, /### Documentation/);
|
|
120
75
|
assert.match(md, /- add a brand new feature[^()]*\(#2\)/);
|
|
121
76
|
assert.match(md, /- fix a critical bug[^()]*\(#1\)/);
|
|
122
|
-
assert.match(md, /- update the documentation[^()]*\(ccc\)/);
|
|
77
|
+
assert.match(md, /- update the documentation[^()]*\(ccc\)/); // sha 后缀(无 PR)
|
|
123
78
|
ok('renderMarkdownEntry 节结构 + PR/sha 后缀');
|
|
79
|
+
// ---- 7. prependEntry:不覆盖 CHANGELOG.md 历史段 ----
|
|
124
80
|
const existing = `# Changelog
|
|
125
81
|
|
|
126
82
|
All notable changes to this project will be documented in this file.
|
|
@@ -138,28 +94,25 @@ assert.match(next, /## \[0\.0\.1-rc\.16\] - 2026-08-30/);
|
|
|
138
94
|
assert.match(next, /## \[0\.0\.1-rc\.15\] - 2026-08-29/);
|
|
139
95
|
assert.match(next, /预订 saga 状态机/);
|
|
140
96
|
ok('prependEntry:新版本段在顶部,历史段原样保留');
|
|
97
|
+
// ---- 8. prependEntry:空文件 → 仅生成 CHANGELOG header + 新段 ----
|
|
141
98
|
const fromEmpty = prependEntry('', newMd);
|
|
142
99
|
assert.match(fromEmpty, /Keep a Changelog/);
|
|
143
100
|
assert.match(fromEmpty, /## \[0\.0\.1-rc\.16\]/);
|
|
144
101
|
ok('prependEntry:空文件首次生成');
|
|
102
|
+
// ---- 9. splitChangelog:preamble 与 history 分割 ----
|
|
145
103
|
const split = splitChangelog(existing);
|
|
146
104
|
assert.ok(split.preamble.includes('All notable changes'));
|
|
147
105
|
assert.ok(split.history[0].includes('## [0.0.1-rc.15]'));
|
|
148
106
|
ok('splitChangelog preamble/history 分隔正确');
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
date: '2026-08-30',
|
|
152
|
-
commits: []
|
|
153
|
-
});
|
|
107
|
+
// ---- 10. buildEntry 空 sections 不崩(空 commit 列表)----
|
|
108
|
+
const emptyEntry = buildEntry({ version: '0.0.1-rc.16', date: '2026-08-30', commits: [] });
|
|
154
109
|
const emptyMd = renderMarkdownEntry(emptyEntry);
|
|
155
110
|
assert.match(emptyMd, /## \[0\.0\.1-rc\.16\]/);
|
|
156
111
|
ok('buildEntry 空 commit 列表 → 仍输出版本头(0 section,合规)');
|
|
112
|
+
// ---- 11. parseCommit:! 标记(breaking change)----
|
|
157
113
|
const breaking = parseCommit('feat(api)!: 重命名端点 /v1/users → /v2/users (#99)');
|
|
158
114
|
assert.equal(breaking.type, 'feat');
|
|
159
115
|
assert.equal(breaking.subject, '重命名端点 /v1/users → /v2/users');
|
|
160
116
|
assert.equal(breaking.prRef, '99');
|
|
161
117
|
ok('parseCommit feat(scope)!: breaking change marker 容错');
|
|
162
118
|
console.log(`changelog tests: ${passed} 组断言全绿(纯函数)`);
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
//# sourceURL=ts/scripts/changelog-tests.ts
|