@danceiny/gotry 0.0.1-rc.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +267 -0
  3. package/bin/gotry-inner.js +214 -0
  4. package/bin/gotry-stdio-ask.js +59 -0
  5. package/bin/gotry.js +28 -0
  6. package/cordis.gotry-patch.yml +86 -0
  7. package/data/flights_2026.json +218 -0
  8. package/data/golden_erhai.json +92 -0
  9. package/data/golden_trip_2026.json +75 -0
  10. package/data/hotels_2026.json +151 -0
  11. package/data/openflights-skeleton.json +895 -0
  12. package/data/time-slot-eval.json +384 -0
  13. package/data/yunnan-pack.json +202 -0
  14. package/dist/capabilities/agent-reach-bridge.py +90 -0
  15. package/dist/capabilities/agent-reach-deep.js +172 -0
  16. package/dist/capabilities/agent-reach.js +204 -0
  17. package/dist/capabilities/anything.js +139 -0
  18. package/dist/capabilities/flyai.js +131 -0
  19. package/dist/capabilities/hbcli.js +143 -0
  20. package/dist/capabilities/incident-log.js +100 -0
  21. package/dist/capabilities/opensky.js +74 -0
  22. package/dist/capabilities/session/action-cache.js +120 -0
  23. package/dist/capabilities/session/adapters/ctrip-flight.js +83 -0
  24. package/dist/capabilities/session/adapters/meituan-local.js +73 -0
  25. package/dist/capabilities/session/extract.js +40 -0
  26. package/dist/capabilities/session/read-guard.js +99 -0
  27. package/dist/capabilities/session/transport.js +101 -0
  28. package/dist/capabilities/session-search.js +93 -0
  29. package/dist/capabilities/weather.js +180 -0
  30. package/dist/scripts/action-cache-tests.js +107 -0
  31. package/dist/scripts/agent-reach-deep-tests.js +54 -0
  32. package/dist/scripts/agent-reach-tests.js +38 -0
  33. package/dist/scripts/agent-reach-wrapper-tests.js +79 -0
  34. package/dist/scripts/anything-tests.js +95 -0
  35. package/dist/scripts/async-collect.js +37 -0
  36. package/dist/scripts/companion-tests.js +112 -0
  37. package/dist/scripts/diff-test.js +39 -0
  38. package/dist/scripts/engine-run.js +14 -0
  39. package/dist/scripts/engine-tests.js +45 -0
  40. package/dist/scripts/hbcli-tests.js +101 -0
  41. package/dist/scripts/incident-tests.js +96 -0
  42. package/dist/scripts/journey-tests.js +45 -0
  43. package/dist/scripts/ledger-tests.js +365 -0
  44. package/dist/scripts/ledger-workflow-crash.js +42 -0
  45. package/dist/scripts/memory-capture-tests.js +77 -0
  46. package/dist/scripts/memory-decay-tests.js +83 -0
  47. package/dist/scripts/memory-metrics.js +24 -0
  48. package/dist/scripts/nudge-digest.js +83 -0
  49. package/dist/scripts/opensky-check.js +20 -0
  50. package/dist/scripts/opensky-tests.js +52 -0
  51. package/dist/scripts/probe-poi-tests.js +88 -0
  52. package/dist/scripts/publish-preverify.js +68 -0
  53. package/dist/scripts/replay-async.js +44 -0
  54. package/dist/scripts/replay-real.js +38 -0
  55. package/dist/scripts/replay.js +116 -0
  56. package/dist/scripts/session-attach-diagnose.js +27 -0
  57. package/dist/scripts/session-attach-poc.js +65 -0
  58. package/dist/scripts/session-extract-tests.js +81 -0
  59. package/dist/scripts/session-tests.js +162 -0
  60. package/dist/scripts/skeleton-check.js +37 -0
  61. package/dist/scripts/skeleton-integration-test.js +22 -0
  62. package/dist/scripts/skills-contract-tests.js +85 -0
  63. package/dist/scripts/smoke.js +328 -0
  64. package/dist/scripts/state-cli-tests.js +136 -0
  65. package/dist/scripts/state-cli.js +248 -0
  66. package/dist/scripts/time-eval-tests.js +318 -0
  67. package/dist/scripts/travel-timeline-tests.js +123 -0
  68. package/dist/scripts/unified-tests.js +49 -0
  69. package/dist/scripts/weather-tests.js +48 -0
  70. package/dist/src/bridge.js +34 -0
  71. package/dist/src/companions.js +112 -0
  72. package/dist/src/contracts.js +64 -0
  73. package/dist/src/dsh-llm.js +174 -0
  74. package/dist/src/engine.js +331 -0
  75. package/dist/src/index.js +1239 -0
  76. package/dist/src/journey.js +147 -0
  77. package/dist/src/loop.js +360 -0
  78. package/dist/src/memory-capture.js +40 -0
  79. package/dist/src/memory-decay.js +31 -0
  80. package/dist/src/memory-utility.js +55 -0
  81. package/dist/src/mock-llm.js +103 -0
  82. package/dist/src/model.js +134 -0
  83. package/dist/src/slot-spec.js +165 -0
  84. package/dist/src/state-ledger.js +901 -0
  85. package/dist/src/time-anchor.js +100 -0
  86. package/dist/src/tool-packet.js +12 -0
  87. package/dist/src/travel-slots.js +144 -0
  88. package/dist/src/travel-timeline.js +78 -0
  89. package/dist/src/unified.js +538 -0
  90. package/dist/src/wish-pool.js +27 -0
  91. package/package.json +84 -0
  92. package/ts/capabilities/agent-reach-deep.ts +114 -0
  93. package/ts/capabilities/agent-reach.ts +158 -0
  94. package/ts/capabilities/anything.ts +201 -0
  95. package/ts/capabilities/flyai.ts +151 -0
  96. package/ts/capabilities/hbcli.ts +152 -0
  97. package/ts/capabilities/incident-log.ts +148 -0
  98. package/ts/capabilities/opensky.ts +121 -0
  99. package/ts/capabilities/session/action-cache.ts +148 -0
  100. package/ts/capabilities/session/adapters/ctrip-flight.ts +107 -0
  101. package/ts/capabilities/session/adapters/meituan-local.ts +70 -0
  102. package/ts/capabilities/session/extract.ts +54 -0
  103. package/ts/capabilities/session/read-guard.ts +141 -0
  104. package/ts/capabilities/session/transport.ts +114 -0
  105. package/ts/capabilities/session-search.ts +129 -0
  106. package/ts/capabilities/weather.ts +176 -0
  107. package/ts/package.json +27 -0
  108. package/ts/scripts/async-collect.ts +50 -0
  109. package/ts/scripts/skeleton-check.ts +49 -0
  110. package/ts/scripts/skeleton-integration-test.ts +23 -0
  111. package/ts/scripts/state-cli.ts +201 -0
  112. package/ts/src/bridge.ts +44 -0
  113. package/ts/src/companions.ts +113 -0
  114. package/ts/src/contracts.ts +165 -0
  115. package/ts/src/dsh-llm.ts +155 -0
  116. package/ts/src/index.ts +968 -0
  117. package/ts/src/loop.ts +411 -0
  118. package/ts/src/memory-capture.ts +59 -0
  119. package/ts/src/memory-decay.ts +55 -0
  120. package/ts/src/memory-utility.ts +70 -0
  121. package/ts/src/mock-llm.ts +100 -0
  122. package/ts/src/model.ts +220 -0
  123. package/ts/src/slot-spec.ts +172 -0
  124. package/ts/src/state-ledger.ts +849 -0
  125. package/ts/src/time-anchor.ts +134 -0
  126. package/ts/src/tool-packet.ts +32 -0
  127. package/ts/src/travel-slots.ts +219 -0
  128. package/ts/src/travel-timeline.ts +83 -0
  129. package/ts/src/unified.ts +582 -0
  130. package/ts/src/wish-pool.ts +66 -0
@@ -0,0 +1,365 @@
1
+ import Database from 'better-sqlite3';
2
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { spawnSync } from 'node:child_process';
6
+ import { ensureLedger, openLedgerIfExists } from '../src/state-ledger.js';
7
+ let pass = 0;
8
+ let fail = 0;
9
+ function assert(cond, msg) {
10
+ if (cond) {
11
+ pass++;
12
+ console.log(` ok - ${msg}`);
13
+ } else {
14
+ fail++;
15
+ console.error(` FAIL - ${msg}`);
16
+ }
17
+ }
18
+ const root = mkdtempSync(join(tmpdir(), 'gotry-ledger-'));
19
+ const ledger = ensureLedger(root);
20
+ const r1 = ledger.appendMotivationPatch({
21
+ weights: {
22
+ escape_rest: 0.7
23
+ },
24
+ evidence: [
25
+ '用户原话:想去湖边什么都不干'
26
+ ],
27
+ hard: {
28
+ wake_not_before: '06:30'
29
+ }
30
+ });
31
+ assert(r1.saved === true, '画像补丁落账(saved)');
32
+ const r2 = ledger.appendMotivationPatch({
33
+ evidence: [
34
+ '用户原话:想去湖边什么都不干'
35
+ ]
36
+ });
37
+ assert(r2.saved === false, '同 evidence 重放幂等(mergeProfile 守门 → 零事件)');
38
+ const before = ledger.countEvents();
39
+ let threw = false;
40
+ try {
41
+ ledger.db.transaction(()=>{
42
+ ledger.insertEvent({
43
+ actor: 'test',
44
+ kind: 'probe',
45
+ payload: {
46
+ x: 1
47
+ }
48
+ });
49
+ throw new Error('boom');
50
+ })();
51
+ } catch {
52
+ threw = true;
53
+ }
54
+ assert(threw && ledger.countEvents() === before, '事务内异常 → 整体回滚,账本无痕(崩溃一致性的单事务证明)');
55
+ threw = false;
56
+ try {
57
+ ledger.appendWish({
58
+ name: '无条件憧憬',
59
+ reason: '',
60
+ conditions: undefined
61
+ });
62
+ } catch {
63
+ threw = true;
64
+ }
65
+ assert(threw && ledger.countEvents() === before, 'conditions 红线在事务内拒绝且零事件(红线进 schema 层)');
66
+ const w1 = ledger.appendWish({
67
+ name: '大理·洱海',
68
+ reason: '',
69
+ conditions: {
70
+ days: 5,
71
+ budget_cny: 4950,
72
+ best_months: [
73
+ 3,
74
+ 4
75
+ ]
76
+ }
77
+ });
78
+ const w2 = ledger.appendWish({
79
+ name: '大理·洱海',
80
+ reason: '两周年想再去',
81
+ conditions: {
82
+ days: 6
83
+ }
84
+ });
85
+ assert(w1.added === true && w2.added === false && w1.wish_id === w2.wish_id, '同名愿望幂等更新,wish_id 语义派生稳定');
86
+ assert(ledger.readWishPool().length === 1, '愿望池投影恰 1 条');
87
+ const u1 = ledger.appendUtilityEvent({
88
+ wish_id: w1.wish_id,
89
+ kind: 'recalled',
90
+ ts: '2026-08-28T00:00:00Z',
91
+ ctx: 'test'
92
+ });
93
+ const u2 = ledger.appendUtilityEvent({
94
+ wish_id: w1.wish_id,
95
+ kind: 'recalled',
96
+ ts: '2026-08-28T00:00:00Z',
97
+ ctx: 'test'
98
+ });
99
+ assert(u1.appended === true && u2.appended === false, '效用事件语义键幂等(重放 no-op)');
100
+ ledger.appendUtilityEvent({
101
+ wish_id: w1.wish_id,
102
+ kind: 'verified_outcome',
103
+ ts: '2026-08-28T00:00:00Z',
104
+ ctx: 'test-noattr'
105
+ });
106
+ assert(ledger.readUtilityEvents().at(-1)?.kind === 'applied', '无归因 verified 降级 applied(六件语义纪律随账本)');
107
+ const t1 = ledger.appendTripEvent({
108
+ destination: '大理',
109
+ start: '2026-10-01',
110
+ source: 'user-verbatim',
111
+ evidence: '用户原话:国庆去了大理'
112
+ });
113
+ const t1dup = ledger.appendTripEvent({
114
+ destination: '大理',
115
+ start: '2026-10-01',
116
+ source: 'user-verbatim',
117
+ evidence: '用户原话:国庆去了大理'
118
+ });
119
+ assert(t1.appended === true && t1dup.appended === false, '行程 trip_id 幂等');
120
+ const tOverlap = ledger.appendTripEvent({
121
+ destination: '大理',
122
+ start: '2026-10-03',
123
+ source: 'user-verbatim',
124
+ evidence: '重叠探针'
125
+ });
126
+ assert(tOverlap.appended === false && /重叠/.test(tOverlap.reason ?? ''), '同目的地日期重叠冲突即停(由人裁决)');
127
+ const tBad = ledger.appendTripEvent({
128
+ destination: 'X地',
129
+ start: '十月一',
130
+ source: 'user-verbatim',
131
+ evidence: 'x'
132
+ });
133
+ assert(tBad.appended === false, '词表外日期拒收(不猜)');
134
+ const cBad = ledger.appendCompanion({
135
+ label: '爸爸',
136
+ constraints: {
137
+ health: [
138
+ '手机号13800001111'
139
+ ]
140
+ },
141
+ evidence: '探针'
142
+ });
143
+ assert(cBad.appended === false && /负面清单/.test(cBad.reason ?? ''), '同行人负面清单拒收(证件/手机号不入库)');
144
+ const cOk = ledger.appendCompanion({
145
+ label: '爸爸',
146
+ constraints: {
147
+ health: [
148
+ '轻度高血压'
149
+ ]
150
+ },
151
+ evidence: '用户原话:爸爸65轻度高血压'
152
+ });
153
+ assert(cOk.appended === true && ledger.readCompanions().length === 1, '同行人正常入账');
154
+ const co = ledger.confirmOutcome({
155
+ wishId: w1.wish_id,
156
+ attribution: 'helpful',
157
+ detail: '成了',
158
+ trip: {
159
+ destination: '大理',
160
+ start: '2026-10-01',
161
+ source: 'wish-confirmed',
162
+ evidence: `wish ${w1.wish_id} confirm-outcome(helpful)`
163
+ }
164
+ });
165
+ assert(co.recorded === true, 'confirm-outcome:verified_outcome 效用事件落账');
166
+ assert(co.trip?.appended === false && /重叠/.test(co.trip?.reason ?? ''), 'confirm-outcome 单事务:行程过守门(重叠拒收,与文件版语义一致)——两写同生或同拒');
167
+ const poolBefore = JSON.stringify(ledger.readWishPool());
168
+ const profileBefore = ledger.readMotivation();
169
+ ledger.db.exec('DELETE FROM projection_docs; DELETE FROM projection_items');
170
+ ledger.rebuildProjections();
171
+ assert(JSON.stringify(ledger.readWishPool()) === poolBefore, 'fold 重建:愿望池与直读逐字节一致');
172
+ assert(JSON.stringify(ledger.readMotivation()?.weights) === JSON.stringify(profileBefore?.weights) && (ledger.readMotivation()?.evidence?.length ?? 0) === (profileBefore?.evidence?.length ?? 0), 'fold 重建:画像与直读一致');
173
+ assert(ledger.readCompanions().length === 1 && ledger.readTrips().length === 1, 'fold 重建:同行人投影与行程日志一致');
174
+ const wishAddedSeqs = ledger.readEvents('wish.added', 10).map((e)=>e.seq);
175
+ const firstWishSeq = Math.min(...wishAddedSeqs);
176
+ ledger.appendWish({
177
+ name: '普吉',
178
+ reason: '',
179
+ conditions: {
180
+ days: 5
181
+ }
182
+ });
183
+ ledger.rebuildProjections(firstWishSeq);
184
+ assert(ledger.readWishPool().length === 1, `rewind 至 seq ${firstWishSeq}:投影回到历史时点(LangGraph fork 同构)`);
185
+ ledger.rebuildProjections();
186
+ assert(ledger.readWishPool().length === 2, 'rebuild 无参回到最新(events 是唯一权威,投影随时可重建)');
187
+ const mroot = mkdtempSync(join(tmpdir(), 'gotry-migrate-'));
188
+ const mdir = join(mroot, 'gotry-state');
189
+ mkdirSync(mdir, {
190
+ recursive: true
191
+ });
192
+ writeFileSync(join(mdir, 'motivation-profile.json'), JSON.stringify({
193
+ weights: {
194
+ curiosity: 0.6
195
+ },
196
+ evidence: [
197
+ '原话A'
198
+ ],
199
+ hard: {},
200
+ updated_at: '2026-08-01T00:00:00Z'
201
+ }));
202
+ writeFileSync(join(mdir, 'wish-pool.json'), JSON.stringify([
203
+ {
204
+ wish_id: 'wLEGACY1',
205
+ name: '京都',
206
+ conditions: {
207
+ days: 7
208
+ },
209
+ added_at: '2026-08-01T00:00:00Z'
210
+ }
211
+ ]));
212
+ writeFileSync(join(mdir, 'memory-utility.jsonl'), JSON.stringify({
213
+ schema: 'memory_utility_observation.v0',
214
+ event_id: 'wLEGACY1|recalled||',
215
+ wish_id: 'wLEGACY1',
216
+ kind: 'recalled',
217
+ ts: '2026-08-02T00:00:00Z'
218
+ }) + '\n');
219
+ writeFileSync(join(mdir, 'trips.jsonl'), JSON.stringify({
220
+ schema: 'travel_timeline.v1',
221
+ trip_id: '京都|2026-04-01|user-verbatim',
222
+ destination: '京都',
223
+ start: '2026-04-01',
224
+ source: 'user-verbatim',
225
+ evidence: '原话',
226
+ ts: '2026-08-01T00:00:00Z'
227
+ }) + '\n');
228
+ writeFileSync(join(mdir, 'companions.json'), JSON.stringify([
229
+ {
230
+ schema: 'companion_profile.v1',
231
+ companion_id: '妈妈',
232
+ label: '妈妈',
233
+ constraints: {
234
+ mobility: '步行≤3h'
235
+ },
236
+ evidence: [
237
+ '原话'
238
+ ],
239
+ ts: '2026-08-01T00:00:00Z'
240
+ }
241
+ ]));
242
+ const m1 = ensureLedger(mroot);
243
+ assert(m1.readMotivation()?.weights?.['curiosity'] === 0.6, '迁移:画像整档入账');
244
+ assert(String(m1.readWishPool()[0]?.wish_id) === 'wLEGACY1', '迁移:愿望保留原主键');
245
+ assert(m1.readUtilityEvents().length === 1 && m1.readTrips().length === 1 && m1.readCompanions().length === 1, '迁移:效用/行程/同行人入账');
246
+ assert(existsSync(join(mdir, 'pre-ledger-backup', 'wish-pool.json')), '迁移:导入前快照存在(pre-ledger-backup/)');
247
+ const migratedCount = m1.countEvents();
248
+ ensureLedger(mroot);
249
+ assert(m1.countEvents() === migratedCount, '重复 ensure 不重复导入(kv 旗标 + 幂等键双保险)');
250
+ const wroot = mkdtempSync(join(tmpdir(), 'gotry-wf-'));
251
+ const countFile = join(wroot, 'solve-count.txt');
252
+ writeFileSync(countFile, '');
253
+ const crash = spawnSync('npx', [
254
+ 'tsx',
255
+ 'scripts/ledger-workflow-crash.ts',
256
+ wroot,
257
+ 'dp-crash1',
258
+ countFile
259
+ ], {
260
+ encoding: 'utf-8'
261
+ });
262
+ assert(crash.status === 9, `崩溃探针按设计 exit 9(实际 ${crash.status}:${(crash.stderr ?? '').slice(0, 200)})`);
263
+ const runRow = openLedgerIfExists(wroot)?.getWorkflowRun('dp-crash1');
264
+ assert(runRow?.status === 'pending', '崩溃后工单仍 pending(账本权威未损)');
265
+ const count1 = readFileSync(countFile, 'utf-8').split('\n').filter(Boolean).length;
266
+ assert(count1 === 1, '崩溃前真实求解恰 1 次');
267
+ const resume = spawnSync('npx', [
268
+ 'tsx',
269
+ 'scripts/async-collect.ts',
270
+ 'dp-crash1',
271
+ wroot
272
+ ], {
273
+ encoding: 'utf-8',
274
+ env: {
275
+ ...process.env,
276
+ GOTRY_SOLVE_COUNT_FILE: countFile
277
+ }
278
+ });
279
+ assert(resume.status === 0, `恢复进程 exit 0(实际 ${resume.status}:${(resume.stderr ?? '').slice(0, 300)})`);
280
+ const count2 = readFileSync(countFile, 'utf-8').split('\n').filter(Boolean).length;
281
+ assert(count2 === 1, '恢复时 done 步骤零重算(exactly-once:求解计数仍 1,不重复花钱)');
282
+ assert(existsSync(join(wroot, 'gotry-state', 'async', 'dp-crash1.deliverable.md')), '交付物视图已落盘(.deliverable.md)');
283
+ assert(openLedgerIfExists(wroot)?.getWorkflowRun('dp-crash1')?.status === 'settled', '账本终态 settled');
284
+ const replay = spawnSync('npx', [
285
+ 'tsx',
286
+ 'scripts/async-collect.ts',
287
+ 'dp-crash1',
288
+ wroot
289
+ ], {
290
+ encoding: 'utf-8',
291
+ env: {
292
+ ...process.env,
293
+ GOTRY_SOLVE_COUNT_FILE: countFile
294
+ }
295
+ });
296
+ assert(replay.status === 0, '终态复诵 exit 0(幂等)');
297
+ const count3 = readFileSync(countFile, 'utf-8').split('\n').filter(Boolean).length;
298
+ assert(count3 === 1, '终态复诵零重算');
299
+ const pw1 = ledger.requestPendingWrite({
300
+ idemKey: 'booking:demo-1',
301
+ seam: 'flight-order-confirm',
302
+ payload: {
303
+ flight: 'MU123'
304
+ }
305
+ });
306
+ const pw2 = ledger.requestPendingWrite({
307
+ idemKey: 'booking:demo-1',
308
+ seam: 'flight-order-confirm',
309
+ payload: {
310
+ flight: 'MU123'
311
+ }
312
+ });
313
+ assert(pw1.created === true && pw2.created === false, 'pending_writes 幂等键去重(同一确认不可能登记两次)');
314
+ const cf1 = ledger.confirmPendingWrite('booking:demo-1', 'PNR-ABC');
315
+ const cf2 = ledger.confirmPendingWrite('booking:demo-1', 'PNR-ABC2');
316
+ assert(cf1.ok === true && cf2.ok === false && cf2.status === 'confirmed', 'L3 确认只能发生一次,二连击被拒');
317
+ const cp1 = ledger.compensatePendingWrite('booking:demo-1', '用户改签退款');
318
+ assert(cp1.ok === true && ledger.listPendingWrites()[0]?.status === 'compensated', 'saga 补偿可达(pending/confirmed → compensated)');
319
+ const audit = ledger.readEvents(undefined, 100).filter((e)=>e.kind.startsWith('write.'));
320
+ assert(audit.some((e)=>e.kind === 'write.pending') && audit.some((e)=>e.kind === 'write.confirmed') && audit.some((e)=>e.kind === 'write.compensated'), '写权审计事件链完整(append-only,WriteGate 的 receipt 落点)');
321
+ const forkPath = join(root, 'whatif.db');
322
+ ledger.forkWhatIf(forkPath);
323
+ const forkDb = new Database(forkPath);
324
+ forkDb.prepare("INSERT INTO events (ts, actor, kind, subject_id, payload) VALUES (?, 'test', 'probe', '', '{}')").run(new Date().toISOString());
325
+ const forkCount = forkDb.prepare('SELECT COUNT(*) AS n FROM events').get().n;
326
+ forkDb.close();
327
+ assert(ledger.countEvents() === forkCount - 1, 'what-if 分叉:副本写入不触正本(VACUUM INTO 预演)');
328
+ const localLedger = ensureLedger(root);
329
+ const u123 = ensureLedger(root, 'u123');
330
+ localLedger.appendWish({
331
+ name: '本地愿望',
332
+ reason: '',
333
+ conditions: {
334
+ days: 3
335
+ }
336
+ });
337
+ u123.appendWish({
338
+ name: '云端愿望',
339
+ reason: '',
340
+ conditions: {
341
+ days: 7
342
+ }
343
+ });
344
+ assert(localLedger.readWishPool().some((w)=>String(w.name) === '本地愿望'), '本地租户读到自己的愿望');
345
+ assert(!localLedger.readWishPool().some((w)=>String(w.name) === '云端愿望'), '本地租户读不到云端愿望(隔离)');
346
+ assert(u123.readWishPool().some((w)=>String(w.name) === '云端愿望'), '云端租户读到自己的愿望');
347
+ assert(!u123.readWishPool().some((w)=>String(w.name) === '本地愿望'), '云端租户读不到本地愿望(隔离)');
348
+ assert(localLedger.tenant === 'local' && u123.tenant === 'u123', 'tenant_id 从第一天就是一等字段');
349
+ rmSync(root, {
350
+ recursive: true,
351
+ force: true
352
+ });
353
+ rmSync(mroot, {
354
+ recursive: true,
355
+ force: true
356
+ });
357
+ rmSync(wroot, {
358
+ recursive: true,
359
+ force: true
360
+ });
361
+ console.log(`\nLEDGER TESTS: ${pass} ok, ${fail} fail`);
362
+ if (fail > 0) process.exit(1);
363
+
364
+
365
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/ledger-tests.ts
@@ -0,0 +1,42 @@
1
+ import { appendFileSync } from 'node:fs';
2
+ import { collectDeepPlanning, makeJournaledSolvePort, persistAsyncTicket } from '../src/loop.js';
3
+ import { openLedgerIfExists } from '../src/state-ledger.js';
4
+ const [root, id, countFile] = process.argv.slice(2);
5
+ if (!root || !id || !countFile) {
6
+ console.error('用法:npx tsx scripts/ledger-workflow-crash.ts <stateRoot> <ticketId> <countFile>');
7
+ process.exit(1);
8
+ }
9
+ const ticket = {
10
+ id,
11
+ objective: '崩溃注入:已求解未交付(settle 前被杀)',
12
+ requestedAt: new Date().toISOString(),
13
+ etaLabel: '秒级'
14
+ };
15
+ const state = {
16
+ calendar: {
17
+ year: 2026,
18
+ assertedWeekdays: {}
19
+ },
20
+ profile: {},
21
+ gates: [],
22
+ wishes: [],
23
+ spec: {
24
+ segments: []
25
+ }
26
+ };
27
+ await persistAsyncTicket(ticket, state, root);
28
+ const ledger = openLedgerIfExists(root);
29
+ const solve = makeJournaledSolvePort(ledger, id, async ()=>({
30
+ feasible: false,
31
+ unsat_core: [
32
+ 'crash-probe'
33
+ ],
34
+ suggestions: []
35
+ }), {
36
+ onRealSolve: ()=>appendFileSync(countFile, 'solve\n')
37
+ });
38
+ await collectDeepPlanning(state, ticket, solve);
39
+ process.exit(9);
40
+
41
+
42
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/ledger-workflow-crash.ts
@@ -0,0 +1,77 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mergeProfile } from '../src/memory-capture.js';
3
+ const cur = {
4
+ weights: {
5
+ escape_rest: 1
6
+ },
7
+ evidence: [
8
+ '用户原话:「想去湖边什么都不干」'
9
+ ],
10
+ hard: {
11
+ wake_not_before: '07:00'
12
+ }
13
+ };
14
+ {
15
+ const m1 = mergeProfile(cur, {
16
+ evidence: [
17
+ '用户原话:「预算 5000 以内」',
18
+ '用户原话:「想去湖边什么都不干」'
19
+ ]
20
+ });
21
+ assert.equal(m1?.evidence.length, 2, `追加新+跳过重复,实际 ${m1?.evidence.length}`);
22
+ assert.ok(m1?.evidence[0].includes('湖边'), '既有 evidence 原位保留(P0)');
23
+ const m2 = mergeProfile(m1, {
24
+ evidence: [
25
+ '用户原话:「预算 5000 以内」'
26
+ ]
27
+ });
28
+ assert.equal(m2, null, '纯重复补丁应返 null(幂等)');
29
+ console.log('1. 追加不删史 + 幂等 OK');
30
+ }{
31
+ const m = mergeProfile(cur, {
32
+ weights: {
33
+ escape_rest: 0.5,
34
+ curiosity: 0.5
35
+ },
36
+ evidence: [
37
+ '用户原话:「这次既要躺平也要探索」'
38
+ ]
39
+ });
40
+ const sum = Object.values(m?.weights ?? {}).reduce((a, b)=>a + b, 0);
41
+ assert.ok(Math.abs(sum - 1) < 0.01, `权重应归一,实际 sum=${sum}`);
42
+ const noEv = mergeProfile(cur, {
43
+ weights: {
44
+ escape_rest: 0.3,
45
+ curiosity: 0.7
46
+ }
47
+ });
48
+ assert.equal(noEv?.weights.escape_rest, 1, '权重变更无新 evidence 应被拒(P0)');
49
+ console.log('2. 权重归一 + P0 证据校验 OK');
50
+ }{
51
+ const first = mergeProfile(null, {
52
+ weights: {
53
+ escape_rest: 1
54
+ },
55
+ evidence: [
56
+ '用户原话:「想去湖边」'
57
+ ]
58
+ });
59
+ assert.ok(first && first.evidence.length === 1 && Object.keys(first.weights).length === 1, '首存应生效');
60
+ assert.equal(mergeProfile(null, null), null, 'null 补丁仍守卫');
61
+ console.log('2.5 首存语义(current=null→空档案) OK');
62
+ }{
63
+ assert.equal(mergeProfile(cur, null), null, 'null 补丁');
64
+ assert.equal(mergeProfile(cur, {}), null, '空补丁');
65
+ const m = mergeProfile(cur, {
66
+ hard: {
67
+ wake_not_before: '08:30',
68
+ budget_cny: 5000
69
+ }
70
+ });
71
+ assert.equal(m?.hard?.wake_not_before, '08:30', 'hard 后到优先');
72
+ assert.equal(m?.hard?.budget_cny, 5000, 'hard 新键并入');
73
+ console.log('3. 空守卫 + hard 覆盖后到优先 OK');
74
+ }console.log('\nMEMORY-MERGE TESTS: 3/3 OK(T1 合并守门层)');
75
+
76
+
77
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/memory-capture-tests.ts
@@ -0,0 +1,83 @@
1
+ import assert from 'node:assert/strict';
2
+ import { decayedConfidence, eventDecayScore, windowFactor } from '../src/memory-decay.js';
3
+ const NOW = new Date(2026, 7, 28, 12);
4
+ let n = 0;
5
+ function pass(name, body) {
6
+ body();
7
+ console.log(` ${++n}. ${name} OK`);
8
+ }
9
+ pass('分级窗口:边界值与地板', ()=>{
10
+ assert.equal(windowFactor(0), 1.0);
11
+ assert.equal(windowFactor(30), 1.0);
12
+ assert.equal(windowFactor(31), 0.75);
13
+ assert.equal(windowFactor(90), 0.75);
14
+ assert.equal(windowFactor(91), 0.5);
15
+ assert.equal(windowFactor(180), 0.5);
16
+ assert.equal(windowFactor(181), 0.25);
17
+ assert.equal(windowFactor(365), 0.25);
18
+ assert.equal(windowFactor(366), 0.1);
19
+ assert.equal(windowFactor(3000), 0.1, '地板 0.1:旧而不灭');
20
+ });
21
+ pass('种类权重:verified > applied > recalled(自称被召回 ≠ 有用)', ()=>{
22
+ const ts = '2026-08-27T00:00:00Z';
23
+ assert.ok(eventDecayScore({
24
+ ts,
25
+ kind: 'verified_outcome'
26
+ }, NOW) > eventDecayScore({
27
+ ts,
28
+ kind: 'applied'
29
+ }, NOW));
30
+ assert.ok(eventDecayScore({
31
+ ts,
32
+ kind: 'applied'
33
+ }, NOW) > eventDecayScore({
34
+ ts,
35
+ kind: 'recalled'
36
+ }, NOW));
37
+ });
38
+ pass('单调:同种类,新事件分 ≥ 旧事件分', ()=>{
39
+ const fresh = eventDecayScore({
40
+ ts: '2026-08-27T00:00:00Z',
41
+ kind: 'recalled'
42
+ }, NOW);
43
+ const old = eventDecayScore({
44
+ ts: '2025-01-01T00:00:00Z',
45
+ kind: 'recalled'
46
+ }, NOW);
47
+ assert.ok(fresh >= old);
48
+ assert.equal(old, 0.025, '地板乘子=0.25×0.1(一年外)');
49
+ });
50
+ pass('置信度上界 1 且多事件累加有界', ()=>{
51
+ const many = Array.from({
52
+ length: 50
53
+ }, ()=>({
54
+ ts: '2026-08-27T00:00:00Z',
55
+ kind: 'verified_outcome'
56
+ }));
57
+ assert.equal(decayedConfidence(many, NOW), 1);
58
+ const one = decayedConfidence([
59
+ {
60
+ ts: '2026-08-27T00:00:00Z',
61
+ kind: 'verified_outcome'
62
+ }
63
+ ], NOW);
64
+ assert.ok(one > 0 && one <= 1);
65
+ });
66
+ pass('动机层零衰减(构造性):本模块无作用于 motivation-profile 的 API,权重原样透传', ()=>{
67
+ const weights = {
68
+ escape_rest: 0.4,
69
+ curiosity: 0.09
70
+ };
71
+ const snapshot = JSON.stringify(weights);
72
+ void decayedConfidence([
73
+ {
74
+ ts: '2020-01-01T00:00:00Z',
75
+ kind: 'recalled'
76
+ }
77
+ ], NOW);
78
+ assert.equal(JSON.stringify(weights), snapshot);
79
+ });
80
+ console.log(`\nMEMORY DECAY TESTS: ${n}/5 OK(memory-design P3 分级窗口/单调/地板/上界/动机零衰减)`);
81
+
82
+
83
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/memory-decay-tests.ts
@@ -0,0 +1,24 @@
1
+ import { projectUtility } from '../src/memory-utility.js';
2
+ import { decayedConfidence } from '../src/memory-decay.js';
3
+ import { readUtilityEventsWithFallback, readWishPoolWithFallback } from '../src/state-ledger.js';
4
+ const stateRoot = process.argv[2] ?? '.';
5
+ const pool = readWishPoolWithFallback(stateRoot);
6
+ const events = readUtilityEventsWithFallback(stateRoot);
7
+ const projection = projectUtility(events);
8
+ const active = pool.filter((w)=>typeof w.wish_id === 'string');
9
+ const recalledWishes = Object.values(projection).filter((w)=>w.recalled > 0).length;
10
+ const verifiedWishes = Object.values(projection).filter((w)=>w.verified > 0).length;
11
+ const refluxBaseline = verifiedWishes / Math.max(recalledWishes, 1);
12
+ console.log('=== GoTry 记忆效用指标(M4 北极星过程面,只读) ===');
13
+ console.log(`wish pool: ${pool.length} 条在册(其中 ${pool.filter((w)=>w.muted).length} 条休眠,${active.length} 条有稳定 wish_id)`);
14
+ console.log(`效用事件: ${events.length} 条(recalled=${events.filter((e)=>e.kind === 'recalled').length}, applied=${events.filter((e)=>e.kind === 'applied').length}, verified=${events.filter((e)=>e.kind === 'verified_outcome').length})`);
15
+ for (const w of Object.values(projection)){
16
+ const name = pool.find((p)=>p.wish_id === w.wish_id)?.name ?? w.wish_id;
17
+ const conf = decayedConfidence(events.filter((e)=>e.wish_id === w.wish_id), new Date());
18
+ console.log(` - ${name}: status=${w.status}, recalled=${w.recalled}, applied=${w.applied}, verified=${w.verified}, 新鲜置信度=${conf}(P3 时间窗衰减)`);
19
+ }
20
+ console.log(`经验回流率基线 = ${verifiedWishes}/${recalledWishes} = ${refluxBaseline.toFixed(2)}(verified/recalled;单用户起步期样本稀疏属预期)`);
21
+ if (pool.length === 0) console.log('(wish pool 为空——首访用户,指标从首条憧憬入池开始积累)');
22
+
23
+
24
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/memory-metrics.ts
@@ -0,0 +1,83 @@
1
+ import { writeFileSync, mkdirSync } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
+ import { pickNudgeWish } from '../src/wish-pool.js';
4
+ import { projectUtility } from '../src/memory-utility.js';
5
+ import { buildTimeAnchor } from '../src/time-anchor.js';
6
+ import { readUtilityEventsWithFallback, readWishPoolWithFallback } from '../src/state-ledger.js';
7
+ function arg(name) {
8
+ const i = process.argv.indexOf(name);
9
+ return i >= 0 ? process.argv[i + 1] : undefined;
10
+ }
11
+ const stateRoot = arg('--state-root') ?? '.';
12
+ const stateDir = join(stateRoot, 'gotry-state');
13
+ function readEvents() {
14
+ return readUtilityEventsWithFallback(stateRoot);
15
+ }
16
+ if (process.env['GOTRY_NUDGE_ENABLED'] === 'false') {
17
+ console.log('回访已关闭(GOTRY_NUDGE_ENABLED=false)——可关闭契约,正常退出');
18
+ process.exit(0);
19
+ }
20
+ const pool = readWishPoolWithFallback(stateRoot);
21
+ const anchor = buildTimeAnchor(new Date());
22
+ const ctx = {
23
+ days: arg('--days') ? Number(arg('--days')) : undefined,
24
+ budgetCny: arg('--budget') ? Number(arg('--budget')) : undefined,
25
+ month: arg('--month') ? Number(arg('--month')) : new Date().getMonth() + 1
26
+ };
27
+ const match = pickNudgeWish(pool, ctx);
28
+ const lines = [
29
+ `# 「下一次出发」回访摘要(${anchor.today} ${anchor.todayWeekdayZh})`
30
+ ];
31
+ if (!match) {
32
+ lines.push(`在册 ${pool.filter((p)=>!p.muted).length} 条憧憬,当前窗口无可成行匹配——不打扰(0..1 纪律:不硬推)。`);
33
+ } else {
34
+ const c = match.entry.conditions ?? {};
35
+ const u = projectUtility(readEvents())[match.wishId];
36
+ lines.push(`**${String(match.entry.name ?? match.wishId)}**(${match.hits.join(' + ')},命中 ${match.score}/3 项)`);
37
+ lines.push(`- 成行条件: ${JSON.stringify(c)}`);
38
+ if (match.entry.reason) lines.push(`- 当初为什么: ${String(match.entry.reason).slice(0, 160)}`);
39
+ lines.push(`- 效用状态: ${u?.status ?? 'unknown'}(被召回 ${u?.recalled ?? 0} 次)`);
40
+ lines.push('');
41
+ lines.push('_想安排就说一声;不想被打扰说「mute <名字>」即可休眠,憧憬不被拒绝。_');
42
+ }
43
+ const digest = lines.join('\n');
44
+ const channel = process.env['GOTRY_NUDGE_CHANNEL'] ?? 'stdout';
45
+ if (channel === 'file') {
46
+ const out = process.env['GOTRY_NUDGE_FILE'] ?? join(stateDir, 'nudge-digest.md');
47
+ mkdirSync(dirname(out), {
48
+ recursive: true
49
+ });
50
+ writeFileSync(out, digest + '\n', 'utf-8');
51
+ console.log(`摘要已写入 ${out}`);
52
+ } else if (channel === 'lark') {
53
+ const webhook = process.env['GOTRY_LARK_WEBHOOK'];
54
+ if (!webhook) {
55
+ console.log('lark 通道未配置(GOTRY_LARK_WEBHOOK 缺失)——降级 stdout,即插即用等 key:');
56
+ console.log(digest);
57
+ } else {
58
+ try {
59
+ const resp = await fetch(webhook, {
60
+ method: 'POST',
61
+ headers: {
62
+ 'Content-Type': 'application/json'
63
+ },
64
+ body: JSON.stringify({
65
+ msg_type: 'text',
66
+ content: {
67
+ text: digest
68
+ }
69
+ })
70
+ });
71
+ console.log(`lark 投递 HTTP ${resp.status}`);
72
+ if (!resp.ok) console.log(digest);
73
+ } catch (e) {
74
+ console.log(`lark 投递失败(${e.message})——降级 stdout:`);
75
+ console.log(digest);
76
+ }
77
+ }
78
+ } else {
79
+ console.log(digest);
80
+ }
81
+
82
+
83
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/nudge-digest.ts