@avantf/dsh-mem 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -123,11 +123,10 @@ export type RecallHitsResult = StoreResult<MemoryStore, 'searchMemory'>;
123
123
  /** The payload of `mem_recall.titles`: an exact page or a fuzzy shortlist of title candidates. */
124
124
  export type RecallTitlesResult = StoreResult<MemoryStore, 'titles'>;
125
125
  /**
126
- * `remember` dispatches to five store operations whose RESULTS differ: `add`/`update` return the
127
- * written fact, `remove` a flag, the feedback actions the new counter (or `null`). One plain
128
- * signature would either say `unknown` — leaving every caller to invent a cast, which is how a
129
- * caller ends up reading a field that does not exist — or a union that forces every caller to
130
- * narrow first. The callable type says what each ACTION returns.
126
+ * `remember` dispatches to three store operations whose RESULTS differ: `add`/`update` return the
127
+ * written fact, `remove` a flag. One plain signature would either say `unknown` — leaving every
128
+ * caller to invent a cast, which is how a caller ends up reading a field that does not exist — or a
129
+ * union that forces every caller to narrow first. The callable type says what each ACTION returns.
131
130
  */
132
131
  export type RememberDispatch = {
133
132
  (req: Extract<RememberRequest, {
@@ -139,9 +138,6 @@ export type RememberDispatch = {
139
138
  (req: Extract<RememberRequest, {
140
139
  action: 'remove';
141
140
  }>): Promise<StoreResult<MemoryStore, 'remove'>>;
142
- (req: Extract<RememberRequest, {
143
- action: 'helpful' | 'unhelpful';
144
- }>): Promise<StoreResult<MemoryStore, 'helpful'>>;
145
141
  (req: RememberRequest, rules?: RememberWriteRules): Promise<unknown>;
146
142
  };
147
143
  /**
@@ -4,13 +4,15 @@
4
4
  *
5
5
  * RESTORE IS NOT IMPORT. `import` / `merge` (see `facts_io.ts`) are CONTENT-IDENTITY UNIONS: new
6
6
  * rows get new ids, only ACTIVE facts travel, and every value the document carries beyond
7
- * kind/ttl/sources/validity is counted as dropped. Restore is the opposite promise — "put this
8
- * library back exactly as it was" — which is only answerable if the target is EMPTY:
7
+ * kind/sources/event time is counted as dropped (the retired `ttl_days` / `valid_to` are ignored).
8
+ * Restore is the opposite promise — "put this library back exactly as it was" — which is only
9
+ * answerable if the target is EMPTY:
9
10
  *
10
11
  * - `facts` is written DIRECTLY, one transaction, keeping `fact_id`, status, timestamps,
11
- * `trust_score`, `settle_clock`, counters, pin state, TTL, validity, the supersedes pointer and
12
- * the mirror bookkeeping. A non-empty target is REFUSED (never merged into), because "restore"
13
- * that silently unions two histories is a different, unreviewable operation.
12
+ * `trust_score`, `settle_clock`, counters, pin state, TTL (format only), event time, the
13
+ * supersedes pointer and the mirror bookkeeping. A non-empty target is REFUSED (never merged
14
+ * into), because "restore" that silently unions two histories is a different, unreviewable
15
+ * operation.
14
16
  * - Rows are inserted in `source_id` ASCENDING order, so the row a `supersedes` pointer names
15
17
  * already exists when the pointer is written. v1 documents carry the same pointer as
16
18
  * `supersedes_source_id`, so both versions resolve identically.
@@ -64,7 +66,7 @@ export declare const RESTORE_VECTOR_HINT: string;
64
66
  * rather than trust). A field counts as missing when the document does not carry the key at all —
65
67
  * `null` is a value the document states ("unknown"), not an absence.
66
68
  */
67
- export declare const RESTORE_METADATA_FIELDS: readonly ["kind", "title", "created_at", "updated_at", "trust_score", "pinned", "pinned_at", "assert_count", "helpful_count", "last_reinforced_at", "ttl_days", "valid_from", "valid_to", "supersedes_source_id", "archive", "sources", "settle_clock", "bonus_count", "bonus_window_at", "retrieval_count", "last_retrieved_at", "mirror_source", "mirror_target", "entities_version", "conflict_checked", "relations"];
69
+ export declare const RESTORE_METADATA_FIELDS: readonly ["kind", "title", "created_at", "updated_at", "trust_score", "pinned", "pinned_at", "assert_count", "helpful_count", "last_reinforced_at", "ttl_days", "valid_from", "supersedes_source_id", "archive", "sources", "settle_clock", "bonus_count", "bonus_window_at", "retrieval_count", "last_retrieved_at", "mirror_source", "mirror_target", "entities_version", "conflict_checked", "relations"];
68
70
  export type RestoreMetadataField = (typeof RESTORE_METADATA_FIELDS)[number];
69
71
  /** Per-field count of facts whose document did not carry that field. */
70
72
  export type RestoreMetadataMissing = Record<RestoreMetadataField, number>;
@@ -164,17 +164,15 @@ export interface SearchInput {
164
164
  /**
165
165
  * P-08 / P-13 write-side extras on `add` / `update`.
166
166
  *
167
- * All three are optional and "omitted" has one meaning everywhere: do not write (a new row's
168
- * NULL/1 default, or the existing row's value on a duplicate) — the store never invents a source,
169
- * an event time or an end date.
167
+ * Both are optional and "omitted" has one meaning everywhere: do not write (a new row's
168
+ * NULL/1 default, or the existing row's value on a duplicate) — the store never invents a source
169
+ * or an event time.
170
170
  */
171
171
  export interface FactWriteOptions {
172
172
  /** P-08: `fact_sources` provenance. `kind:` prefix selects the kind, else `manual`. */
173
173
  sourceRef?: string;
174
174
  /** P-13: event time → `valid_from`. */
175
175
  eventDate?: string;
176
- /** P-13: known end → `valid_to`, WITHOUT archiving the row. */
177
- validUntil?: string;
178
176
  /**
179
177
  * The journal title (JOURNAL_TITLE_SEARCH §2.2/§11.1). `undefined` = not given, `null` = "no
180
178
  * title", a string = the (already normalized) title. On a revision an omitted title INHERITS the
@@ -340,7 +338,7 @@ export declare class MemoryStore {
340
338
  * and every existing assertion on it — is unchanged; only the warning's wording uses it.
341
339
  */
342
340
  private vectorSpaceCounts;
343
- add(content: string, kind?: MemoryKind, ttlDays?: number, opts?: FactWriteOptions): Promise<RememberResult>;
341
+ add(content: string, kind?: MemoryKind, opts?: FactWriteOptions): Promise<RememberResult>;
344
342
  /**
345
343
  * Write-path contradiction detection (DESIGN §11): check the fact that was just
346
344
  * written against the corpus, then report its FULL open-conflict set.
@@ -367,24 +365,16 @@ export declare class MemoryStore {
367
365
  private warnIndexFailure;
368
366
  /** Best-effort semantic indexing: encode the fact and add it to the vstore. */
369
367
  private maybeIndexSemantic;
370
- update(fact_id: number, content: string, kind?: MemoryKind, ttlDays?: number, opts?: FactWriteOptions): Promise<RememberResult>;
368
+ update(fact_id: number, content: string, kind?: MemoryKind, opts?: FactWriteOptions): Promise<RememberResult>;
371
369
  remove(fact_id: number, reason?: string): Promise<boolean>;
372
370
  archive(fact_id: number, reason?: string): Promise<boolean>;
373
371
  /**
374
- * Bring an archived fact back (spec §2.6): a `forgot` fact is lifted to
375
- * `recall_floor` (otherwise the very next tick would re-archive it, R4), the
376
- * idle clock is refreshed (otherwise an `idle`-archived fact dies again, R18),
372
+ * Bring an archived fact back (spec §2.6): a `forgot` fact is lifted to the frozen base
373
+ * `BASE_TRUST` (otherwise the very next tick would re-archive it, R4),
374
+ * `last_retrieved_at` is refreshed (observation only — no clock reads it any more),
377
375
  * and `settle_clock` moves to now so the downtime is not charged.
378
376
  */
379
377
  restore(fact_id: number): Promise<boolean>;
380
- helpful(fact_id: number): Promise<number | null>;
381
- unhelpful(fact_id: number): Promise<number | null>;
382
- /**
383
- * Explicit feedback (spec §2.4). Pinned/archived rows only move `helpful_count`
384
- * (R7); otherwise the decay is settled first, then `±feedback_delta`, pin at the
385
- * permanent threshold (snap to 1.0) or archive immediately at the forget line.
386
- */
387
- private applyFeedback;
388
378
  /** Permanent-memory administration (spec §6). */
389
379
  pin(fact_id: number): Promise<boolean>;
390
380
  unpin(fact_id: number): Promise<boolean>;
@@ -488,7 +478,13 @@ export declare class MemoryStore {
488
478
  * asks "how big is the store right now" must not run a database query to answer.
489
479
  */
490
480
  liveVectorCount(): number;
491
- /** List contradiction candidates (`resolved` 0 = open, 1 = closed with a recorded verdict). */
481
+ /**
482
+ * List contradiction candidates (`resolved` 0 = open, 1 = closed with a recorded verdict).
483
+ *
484
+ * `detected_at` is mapped to the model face's instant shape here (the DAO hands back the raw UTC
485
+ * column): this payload is what the 记忆 tab's 未处理矛盾 table and the CLI both render, and it must
486
+ * not be the one surface still printing a zone-less stamp.
487
+ */
492
488
  listContradictions(opts?: {
493
489
  resolved?: number;
494
490
  kind?: MemoryKind;
@@ -728,13 +724,21 @@ export declare class MemoryStore {
728
724
  * ({@link timePage}, `mode:"time"`): the window is the whole question, so there is nothing to
729
725
  * match, plan or rank.
730
726
  *
731
- * The exact, facts and relation passes reinforce the facts they return, exactly like the fuzzy
732
- * pass does for its hits: all are recalls the caller actually reads. A caller that must stay
733
- * read-only passes `track: false` (the conditional hint's probe does). Exact, facts, relation and
734
- * time also honour the caller's output budget (their text is the caller's to bound). The one
735
- * difference is the DEFAULT: a time window applies no budget unless the caller names one — its
736
- * page size (`limit`) is what bounds the list, and the budget would only rewrite rows the caller
737
- * asked for (see {@link timePage}).
727
+ * What a pass does to the rows it returns is one decision, and it is deliberately NOT uniform:
728
+ *
729
+ * - the RELATION and FUZZY passes REINFORCE them ({@link reinforce}): each answers a question
730
+ * the caller asked, so the rows are what it read;
731
+ * - the TIME, EXACT and FACTS passes record USAGE ONLY ({@link touchOnly}): they hand back a
732
+ * LIST the caller browses — page 2 of a listing is not a second recall — so "it was looked
733
+ * at" still moves `retrieval_count` / `last_retrieved_at` (the dormancy clock) while
734
+ * `trust_score` / `bonus_count` stay where they are.
735
+ *
736
+ * A caller that must stay read-only passes `track: false` (the conditional hint's probe does);
737
+ * that branch writes nothing at all, usage included. Exact, facts, relation and time also honour
738
+ * the caller's output budget (their text is the caller's to bound). The one difference is the
739
+ * DEFAULT: a time window applies no budget unless the caller names one — its page size (`limit`)
740
+ * is what bounds the list, and the budget would only rewrite rows the caller asked for (see
741
+ * {@link timePage}).
738
742
  *
739
743
  * It does NOT emit a retrieval-health event. Those counters describe the retrieval PIPELINE
740
744
  * (legs, fusion, semantic-live rate); none of an exact, facts, relation or time answer ran a leg,
@@ -762,8 +766,9 @@ export declare class MemoryStore {
762
766
  * list", and the budget stays a knob for a RANKED result (exact/facts/relation/hybrid unchanged).
763
767
  * `max_tokens` (including the contract's `0` = unlimited) is passed through unchanged.
764
768
  *
765
- * It honours `kind`/`source`, and reinforces the rows it returns unless `track: false` — a window
766
- * the caller reads is a recall like any other.
769
+ * It honours `kind`/`source`, and records usage ONLY for the rows it returns unless `track: false`
770
+ * ({@link touchOnly}). A window is listed so the caller can look through it, not recalled into an
771
+ * answer: the dormancy clock (`retrieval_count` / `last_retrieved_at`) moves, trust does not.
767
772
  */
768
773
  private timePage;
769
774
  /**
@@ -800,8 +805,8 @@ export declare class MemoryStore {
800
805
  * malformed window is the caller's error exactly as it is in the exact page;
801
806
  * - the page is FACT-level (`total`/`offset`/`limit`/`truncated`), and the caller's output budget
802
807
  * only shortens `content` — dropping a row would make `total` disagree with the page;
803
- * - the returned facts are reinforced unless `track: false`, like every other recall the caller
804
- * actually reads.
808
+ * - the returned facts get usage recorded but NO reinforcement unless `track: false`: a facts
809
+ * page is a list the caller browses, so only the dormancy clock moves ({@link touchOnly}).
805
810
  */
806
811
  private factPage;
807
812
  /**
@@ -975,6 +980,17 @@ export declare class MemoryStore {
975
980
  flushHealth(): Promise<void>;
976
981
  /** Retrieval bookkeeping: `retrieval_count` + the "last used" clock (spec §2.5). */
977
982
  private touchUsage;
983
+ /**
984
+ * Record the USAGE of these facts and nothing else — no trust bonus, no quota, no
985
+ * `last_reinforced_at`.
986
+ *
987
+ * This is the paginated passes' half of {@link reinforce}: `timePage` / `exactContentPage` /
988
+ * `factPage` return a list the caller browses rather than a recall it acted on, so "someone
989
+ * looked at this row" still moves `retrieval_count` / `last_retrieved_at` (observation and
990
+ * export only — no lifecycle step reads that clock) while `trust_score` / `bonus_count` stay
991
+ * put.
992
+ */
993
+ private touchOnly;
978
994
  /**
979
995
  * Semantic leg. `queryVector` short-circuits the encode when the caller already
980
996
  * encoded this exact query (the cross-store router does, for both stores at once);
@@ -1022,13 +1038,11 @@ export declare class MemoryStore {
1022
1038
  /**
1023
1039
  * Insert (or resolve to) a fact row.
1024
1040
  *
1025
- * `kind` / `ttlDays` are `undefined` when the CALLER specified nothing — the
1026
- * distinction is load-bearing. A duplicate-content hit must not clobber the existing
1027
- * row's values with the insert defaults: re-adding a fact without a kind used to
1028
- * reset it to `general`, and reviving an archived one silently cleared an explicit TTL.
1029
- * The rule for all three branches is therefore: an explicitly provided value lands, an
1030
- * omitted one keeps what the row already has, and a brand-new row falls back to the
1031
- * defaults (`general` / `0`).
1041
+ * `kind` is `undefined` when the CALLER specified nothing — the distinction is load-bearing.
1042
+ * A duplicate-content hit must not clobber the existing row's value with the insert default:
1043
+ * re-adding a fact without a kind used to reset it to `general`. The rule for all three
1044
+ * branches is therefore: an explicitly provided value lands, an omitted one keeps what the row
1045
+ * already has, and a brand-new row falls back to the default (`note`).
1032
1046
  */
1033
1047
  private persistFact;
1034
1048
  /**
@@ -107,7 +107,7 @@ export declare function timeParams(range: TimeRange | undefined): (string | numb
107
107
  */
108
108
  export declare function dayInZone(ms: number, timeZone?: string): string;
109
109
  /**
110
- * One stored `valid_from`/`valid_to` string → epoch ms, using the SAME grammar as the filters.
110
+ * One stored event-time (`valid_from`) string → epoch ms, using the SAME grammar as the filters.
111
111
  *
112
112
  * This is what the write path stamps into `valid_from_ms` and what migration 12's backfill applies to
113
113
  * existing rows, so "the event time a caller wrote" and "the event time a window compares" can never
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * The trust heartbeat: the interval beat that advances the active-day clock and performs the
3
- * settle / forget / idle / purge sweeps (TRUST_MODEL.md §5), shared by the DSH plugin and the MCP
3
+ * settle / forget / purge sweeps (TRUST_MODEL.md §5), shared by the DSH plugin and the MCP
4
4
  * server.
5
5
  *
6
6
  * The two surfaces used to install it separately, and the copies drifted in exactly the places a
@@ -182,20 +182,20 @@ export declare const ConfigSchema: z.ZodObject<{
182
182
  }, z.core.$strip>>;
183
183
  trust: z.ZodPrefault<z.ZodObject<{
184
184
  enabled: z.ZodDefault<z.ZodBoolean>;
185
- start: z.ZodDefault<z.ZodNumber>;
186
- decay_per_day: z.ZodDefault<z.ZodNumber>;
185
+ base_lifetime_days: z.ZodDefault<z.ZodNumber>;
186
+ max_retention_days: z.ZodDefault<z.ZodNumber>;
187
+ decay_shape: z.ZodDefault<z.ZodEnum<{
188
+ linear: "linear";
189
+ accelerating: "accelerating";
190
+ }>>;
191
+ decay_curvature: z.ZodDefault<z.ZodNumber>;
187
192
  forget_threshold: z.ZodDefault<z.ZodNumber>;
188
- permanent_threshold: z.ZodDefault<z.ZodNumber>;
189
- recall_floor: z.ZodDefault<z.ZodNumber>;
190
- recall_delta: z.ZodDefault<z.ZodNumber>;
193
+ recall_gain_below_base: z.ZodDefault<z.ZodNumber>;
194
+ recall_gain_above_base: z.ZodDefault<z.ZodNumber>;
191
195
  recall_daily_cap: z.ZodDefault<z.ZodNumber>;
192
196
  recall_marginal_decay: z.ZodDefault<z.ZodNumber>;
193
- recall_ceiling: z.ZodDefault<z.ZodNumber>;
194
- feedback_delta: z.ZodDefault<z.ZodNumber>;
195
- feedback_daily_cap: z.ZodDefault<z.ZodNumber>;
196
197
  inherit_trust_on_update: z.ZodDefault<z.ZodBoolean>;
197
198
  purge_skips_pinned: z.ZodDefault<z.ZodBoolean>;
198
- idle_calendar_days: z.ZodDefault<z.ZodNumber>;
199
199
  tick_max_facts: z.ZodDefault<z.ZodNumber>;
200
200
  presence: z.ZodPrefault<z.ZodObject<{
201
201
  mode: z.ZodDefault<z.ZodEnum<{
@@ -1,8 +1,10 @@
1
1
  export * from './types.js';
2
2
  export * from './config.js';
3
+ export * from './trust_curve.js';
3
4
  export * from './family.js';
4
5
  export * from './tools.js';
5
6
  export * from './remote.js';
6
7
  export * from './log.js';
7
8
  export * from './env.js';
8
9
  export * from './text.js';
10
+ export * from './time.js';
@@ -0,0 +1,75 @@
1
+ /**
2
+ * The ONE place a stored timestamp turns into a string a human (or a model) reads.
3
+ *
4
+ * ── THE BUG THIS MODULE EXISTS FOR ───────────────────────────────────────────────────────────────
5
+ *
6
+ * `facts.created_at` / `updated_at` / `archived_at` (and `contradiction_log.detected_at`, and the
7
+ * retrieval-health stamp) are written by SQLite's `DEFAULT CURRENT_TIMESTAMP`, which is **UTC**, and
8
+ * `store/memory.ts` used to hand the raw text up to every surface. Two consequences, both reported as
9
+ * "记忆时间显示不对":
10
+ *
11
+ * 1. the UI printed `2026-10-28 11:58:48` as if it were a local wall clock (Asia/Shanghai really
12
+ * meant `19:58:48`), because a zone-less string cannot say otherwise;
13
+ * 2. the MODEL received the same zone-less string next to `event_date`, which is a calendar DATE
14
+ * with no time and no zone. Two different kinds of value, one shape — the model had to guess
15
+ * which one it was looking at, and it cannot.
16
+ *
17
+ * ── THE SPLIT THIS MODULE ENFORCES ───────────────────────────────────────────────────────────────
18
+ *
19
+ * Storage stays UTC and stays byte-identical (`facts_io` export/import/snapshot is a compatibility
20
+ * surface and is deliberately NOT routed through here). The fix is in the two DISPLAY faces:
21
+ *
22
+ * - {@link formatOffsetIso} — the MODEL face (`mem_recall` hits, `mem_admin list/detail/
23
+ * contradictions`, `mem_admin stats`, the CLI and MCP that print those payloads): an ISO-8601
24
+ * instant **with an explicit offset**, e.g. `2026-10-28T19:58:48+08:00`, rendered in the host's
25
+ * local zone. The offset is part of the string, so the model needs no conversion and the value is
26
+ * never ambiguous again — while `event_date` stays `2026-10-28`, a calendar date with no zone.
27
+ * - {@link formatLocalHuman} — the HUMAN face (the 记忆 tab): `2026-10-28 19:58`, the same instant,
28
+ * local, to the minute.
29
+ *
30
+ * Both read the input through the SAME parser ({@link parseUtcTs}) and the same instant, so the two
31
+ * faces cannot disagree. The UI deliberately formats the model face's OWN string rather than
32
+ * re-deriving one from the raw column: the input it gets already carries an offset, `parseUtcTs`
33
+ * leaves an explicit offset alone, and there is therefore exactly ONE parse rule and ONE pair of
34
+ * renderers in the repository — not a second opinion in the browser.
35
+ *
36
+ * Dependency-free on purpose: this module is reachable from the DSH client bundle through the
37
+ * `@avantf/mem-contract/time` subpath (the bundle must not pull in the tool schemas / zod). It has no
38
+ * DSH host and no DB, so the engine, the CLI and the MCP face can all use it.
39
+ *
40
+ * @module contract/time
41
+ */
42
+ /**
43
+ * `'YYYY-MM-DD HH:MM:SS'` (SQLite UTC) or ISO → epoch ms; null/garbage → null.
44
+ *
45
+ * A stamp with NO zone designator is read as UTC, not as local time. `Date.parse` treats a zone-less
46
+ * ISO string as LOCAL, and everything in this store is written in UTC (`CURRENT_TIMESTAMP`, and
47
+ * {@link formatUtcTs}), so handing one over as-is would shift every quota window and every decay
48
+ * measurement by the host's UTC offset — a fact reinforced at 23:00 UTC+8 would look reinforced 8
49
+ * hours earlier, and the 24h cap would open or close at the wrong moment. An explicit `Z` or
50
+ * `±HH:MM` is left alone (that is what makes a `formatOffsetIso` output safe to feed back in).
51
+ */
52
+ export declare function parseUtcTs(ts: string | null | undefined): number | null;
53
+ /** Epoch ms → `'YYYY-MM-DD HH:MM:SS'` (the SQLite `CURRENT_TIMESTAMP` shape, UTC). */
54
+ export declare function formatUtcTs(ms: number): string;
55
+ /**
56
+ * The MODEL face: an instant as ISO-8601 with an explicit offset, in the host's local zone
57
+ * (`2026-10-28T19:58:48+08:00`).
58
+ *
59
+ * `null` — never `''` and never a fabricated stamp — when the value is absent or unreadable, so a
60
+ * caller keeps its own "unknown" spelling (`''` for `created_at`, `null` for `updated_at`) instead of
61
+ * rendering an empty time. `+00:00` (not `Z`) on a UTC host: the shape MUST carry a sign, which is
62
+ * what a caller regex-checks.
63
+ */
64
+ export declare function formatOffsetIso(ts: string | null | undefined): string | null;
65
+ /**
66
+ * The HUMAN face: the same instant as local wall-clock text to the minute (`2026-10-28 19:58`).
67
+ *
68
+ * Seconds are deliberately dropped: the list/detail lines and the health line are read at a glance,
69
+ * and `HH:MM` is the whole useful resolution there. `null` for an absent/unreadable value (same
70
+ * contract as {@link formatOffsetIso}), so a caller prints its own 「未知」/`—` placeholder.
71
+ *
72
+ * NOT for `event_date` / `valid_from`: those are calendar dates the CALLER wrote (`2026-10-28`), with
73
+ * no time and no zone. Running one through here would invent a midnight in the host's zone.
74
+ */
75
+ export declare function formatLocalHuman(ts: string | null | undefined): string | null;
@@ -91,7 +91,7 @@ export declare function modelFacingToolResult(toolKey: string, action: unknown,
91
91
  */
92
92
  export declare const EXPORT_STATUSES: readonly ["active", "archived", "all"];
93
93
  export type ExportStatus = StatusFilter;
94
- export declare const REMEMBER_ACTIONS: readonly ["add", "update", "remove", "helpful", "unhelpful"];
94
+ export declare const REMEMBER_ACTIONS: readonly ["add", "update", "remove"];
95
95
  export declare const RememberUnion: z.ZodDiscriminatedUnion<[z.ZodObject<{
96
96
  action: z.ZodLiteral<"add">;
97
97
  content: z.ZodString;
@@ -100,10 +100,8 @@ export declare const RememberUnion: z.ZodDiscriminatedUnion<[z.ZodObject<{
100
100
  note: "note";
101
101
  }>>;
102
102
  title: z.ZodOptional<z.ZodString>;
103
- ttl_days: z.ZodOptional<z.ZodNumber>;
104
103
  source_ref: z.ZodOptional<z.ZodString>;
105
104
  event_date: z.ZodOptional<z.ZodString>;
106
- valid_until: z.ZodOptional<z.ZodString>;
107
105
  relations: z.ZodOptional<z.ZodArray<z.ZodObject<{
108
106
  subject: z.ZodString;
109
107
  predicate: z.ZodString;
@@ -119,10 +117,8 @@ export declare const RememberUnion: z.ZodDiscriminatedUnion<[z.ZodObject<{
119
117
  }>>;
120
118
  title: z.ZodOptional<z.ZodString>;
121
119
  retitle: z.ZodOptional<z.ZodLiteral<true>>;
122
- ttl_days: z.ZodOptional<z.ZodNumber>;
123
120
  source_ref: z.ZodOptional<z.ZodString>;
124
121
  event_date: z.ZodOptional<z.ZodString>;
125
- valid_until: z.ZodOptional<z.ZodString>;
126
122
  relations: z.ZodOptional<z.ZodArray<z.ZodObject<{
127
123
  subject: z.ZodString;
128
124
  predicate: z.ZodString;
@@ -132,12 +128,6 @@ export declare const RememberUnion: z.ZodDiscriminatedUnion<[z.ZodObject<{
132
128
  action: z.ZodLiteral<"remove">;
133
129
  fact_id: z.ZodNumber;
134
130
  reason: z.ZodOptional<z.ZodString>;
135
- }, z.core.$strip>, z.ZodObject<{
136
- action: z.ZodLiteral<"helpful">;
137
- fact_id: z.ZodNumber;
138
- }, z.core.$strip>, z.ZodObject<{
139
- action: z.ZodLiteral<"unhelpful">;
140
- fact_id: z.ZodNumber;
141
131
  }, z.core.$strip>], "action">;
142
132
  /**
143
133
  * The runtime/CLI-visible write request. The base zod union keeps `kind` optional so the CLI's
@@ -146,9 +136,13 @@ export declare const RememberUnion: z.ZodDiscriminatedUnion<[z.ZodObject<{
146
136
  */
147
137
  export type RememberRequest = z.infer<typeof RememberUnion>;
148
138
  /**
149
- * The writes the model may make. `helpful` / `unhelpful` are operator feedback: the model cannot
150
- * observe the retention clocks they move, and a model that could would be able to "refresh" a fact by
151
- * voting on it — the exact behaviour the retention policy exists to prevent.
139
+ * The writes the model may make — `MODEL_REMEMBER_ACTIONS` and `REMEMBER_ACTIONS` are the same three
140
+ * again since the operator-side `helpful` / `unhelpful` actions were removed: a vote on a fact was a
141
+ * way to move its retention clock, and a model able to vote could "refresh" a fact by voting on it.
142
+ *
143
+ * Permanent is set by ONE route only: an explicit `pin` from a human (`mem_admin` / CLI / settings
144
+ * page). Do NOT re-add an action that promotes or demotes a fact from a write path — that is the
145
+ * behaviour the retention policy exists to prevent.
152
146
  */
153
147
  export declare const MODEL_REMEMBER_ACTIONS: readonly ["add", "update", "remove"];
154
148
  export declare const ModelRememberUnion: z.ZodDiscriminatedUnion<readonly z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>[], string>;
@@ -341,7 +335,7 @@ export declare const TOOL_SPECS: ToolSpec[];
341
335
  *
342
336
  * Descriptions are authored per action ("action=update 时必填"), so taking the
343
337
  * first branch's text verbatim told a model that `fact_id` only matters for
344
- * `update` although `remove/helpful/unhelpful` require it too. The action-clause
338
+ * `update` although `remove` requires it too. The action-clause
345
339
  * is stripped and replaced by the derived set of actions that make the field
346
340
  * required.
347
341
  *
@@ -0,0 +1,119 @@
1
+ /**
2
+ * The trust decay curve — PURE functions, no IO, no config object of its own.
3
+ *
4
+ * This file is the ONE implementation of "how much trust is left after N active days" and of every
5
+ * inverse of it. `core/lifecycle/trust.ts` (read path, recall reinforcement), the tick's settle
6
+ * statement (`db/dao/facts.ts`) and the config's load-time validation (`config.ts`) all delegate
7
+ * here, so a change of shape cannot leave one of them behind (docs/TRUST_MODEL.md §2.2, DESIGN §18).
8
+ *
9
+ * It lives in the CONTRACT package because the load-time validation needs the same math, and the
10
+ * dependency direction is core → contract, never the reverse.
11
+ *
12
+ * ## The two shapes
13
+ *
14
+ * `linear` (TRUST_MODEL D7, the P3 equivalent tier): trust falls by a constant amount per active
15
+ * day, `slope = BASE_TRUST / base_lifetime_days` ⇒ a fact that starts at `BASE_TRUST` reaches 0
16
+ * after exactly `base_lifetime_days` active days.
17
+ *
18
+ * `accelerating` (the v2 target shape): `dT/dt = −λ·(a + 1 − T)` — the closer to 1, the slower the
19
+ * decay, so "almost permanent" facts really linger. Closed form (plan §3.1):
20
+ *
21
+ * T(t) = (1 + a) − ((1 + a) − T₀)·e^(λt) λ = ln((1 + a)/(1 + a − BASE_TRUST)) / B
22
+ *
23
+ * Note the POSITIVE exponent: `u = a + 1 − T` grows, so `e^(λt)` is the decay direction here (the
24
+ * plan writes it this way, and the closed-form test pins it). `a = decay_curvature`.
25
+ *
26
+ * ## Units
27
+ *
28
+ * `days` is ALWAYS the ACTIVE-DAY counter (`avantf_stats.trust_clock`), never wall time.
29
+ */
30
+ /**
31
+ * The frozen anchor (TRUST_MODEL D20): a new fact's initial trust, the trust value the lifetime
32
+ * anchor is defined at (`daysToZero(BASE_TRUST) === base_lifetime_days`), and the divider between
33
+ * the two recall-reinforcement segments. One constant on purpose — the three uses must never be
34
+ * configurable apart, or "a new fact lives 90 active days" stops being true.
35
+ */
36
+ export declare const BASE_TRUST = 0.5;
37
+ /** The decay shapes `decay_shape` accepts. */
38
+ export declare const DECAY_SHAPES: readonly ["linear", "accelerating"];
39
+ export type DecayShape = (typeof DECAY_SHAPES)[number];
40
+ /** The curve parameters, flattened from the `trust` config section (no config import: structural). */
41
+ export interface TrustCurve {
42
+ shape: DecayShape;
43
+ /** `decay_curvature` — the floor term `a` of the accelerating shape; unused by `linear`. */
44
+ curvature: number;
45
+ /** `base_lifetime_days` — the active days `BASE_TRUST → 0` takes. */
46
+ baseDays: number;
47
+ }
48
+ /** Read the curve out of the `trust` section (the single mapping from keys to math). */
49
+ export declare function curveOf(trust: {
50
+ decay_shape: DecayShape;
51
+ decay_curvature: number;
52
+ base_lifetime_days: number;
53
+ }): TrustCurve;
54
+ /**
55
+ * Trust units lost per active day — the LINEAR slope, `BASE_TRUST / base_lifetime_days`.
56
+ *
57
+ * `linear` is the only shape with a constant slope; for `accelerating` this returns the LOCAL slope
58
+ * at the anchor `T = BASE_TRUST` (`λ·(BASE_TRUST + a)`). It exists as the building block of the
59
+ * `linear` branch of {@link trustAfterDays} / {@link ceilingForDays} and for the tests that pin the
60
+ * anchor; it is deliberately NOT a scalar stand-in for the curve — anything that needs "how much
61
+ * trust is left after N days" uses {@link trustAfterDays}, and anything that needs "N days from a
62
+ * threshold" uses {@link ceilingForDays}. (An earlier caller — `trust_diagnose.forgetting_soon` —
63
+ * used it as a linear rate and was wrong once `accelerating` became the default.)
64
+ */
65
+ export declare function slopeOf(shape: DecayShape, curvature: number, baseDays: number): number;
66
+ /**
67
+ * `λ = ln((1 + a) / (1 + a − BASE_TRUST)) / baseDays` — pinned so the anchor holds:
68
+ * `λ·baseDays = ln((1 + a)/(0.5 + a))`, i.e. `daysToZero(BASE_TRUST) === baseDays` for ANY `a`.
69
+ */
70
+ export declare function lambdaFor(curvature: number, baseDays: number): number;
71
+ /**
72
+ * `T − days·slope` (linear) or the closed form `(1 + a) − ((1 + a) − T)·e^(λ·days)`.
73
+ *
74
+ * UNCLAMPED on purpose: callers clamp to `[0, 1]` themselves (`effectiveTrust`'s read path and the
75
+ * settle write both must clamp, and a shared clamp here would hide a caller that forgot).
76
+ */
77
+ export declare function trustAfterDays(T: number, days: number, cfg: TrustCurve): number;
78
+ /**
79
+ * Active days between two trust values, `t(T0 → T1)`.
80
+ *
81
+ * `linear`: `(T0 − T1) / slope`; `accelerating`: `ln((1 + a − T1)/(1 + a − T0)) / λ`. Both are
82
+ * signed the natural way (a RISING target yields a negative number of days), so the formula is
83
+ * usable as an inverse without special cases. Returns `NaN` only for an out-of-domain input
84
+ * (`T > 1 + a` on the accelerating branch), which the config validation makes unreachable.
85
+ */
86
+ export declare function daysBetween(T0: number, T1: number, cfg: TrustCurve): number;
87
+ /** Active days until the value reaches 0 — `remainingDays`'s whole body. */
88
+ export declare function daysToZero(T: number, cfg: TrustCurve): number;
89
+ /**
90
+ * The trust ceiling a `max_retention_days` promises: after being reinforced up to `ceiling`, a fact
91
+ * survives `days` more active days.
92
+ *
93
+ * `linear`: `days · slope` (= `days / (2·baseDays)`); `accelerating`: `(1 + a)(1 − e^(−λ·days))`.
94
+ * This is an INVERSION (`daysToZero(ceilingForDays(R)) === R`), not a configuration value — the
95
+ * config has no `recall_ceiling` key any more (D16).
96
+ */
97
+ export declare function ceilingForDays(days: number, cfg: TrustCurve): number;
98
+ /**
99
+ * The inverse of {@link ceilingForDays}: how many active days a given ceiling is worth. Used by the
100
+ * deprecated-key warning (`config/loader.ts`) to translate an old `recall_ceiling` value, and by the
101
+ * validation messages.
102
+ */
103
+ export declare function retentionDaysForCeiling(ceiling: number, cfg: TrustCurve): number;
104
+ /**
105
+ * `R_max` of the accelerating shape — the days at which the ceiling reaches 1 (`ceiling → ∞`).
106
+ *
107
+ * R_max = baseDays · ln((1 + a)/a) / ln((1 + a)/(1 + a − BASE_TRUST))
108
+ *
109
+ * `max_retention_days` must stay strictly below it, or a reinforced fact would stop aging like
110
+ * `pinned` does (D14: permanent is set only by a person).
111
+ */
112
+ export declare function RMAX(curvature: number, baseDays: number): number;
113
+ /**
114
+ * The shape-aware upper bound on `max_retention_days`: the days at which the ceiling hits 1.
115
+ *
116
+ * `linear`: `2·baseDays` (the ceiling is `days/(2·baseDays)`); `accelerating`: {@link RMAX}. Both
117
+ * are STRICT bounds — at the bound the ceiling is exactly 1, which `superRefine` rejects.
118
+ */
119
+ export declare function retentionDaysLimit(shape: DecayShape, curvature: number, baseDays: number): number;