hippo-memory 1.29.0 → 1.31.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.
- package/README.md +13 -0
- package/dist/api.d.ts +49 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +78 -2
- package/dist/api.js.map +1 -1
- package/dist/audit.d.ts +1 -1
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js.map +1 -1
- package/dist/capture.d.ts.map +1 -1
- package/dist/capture.js +79 -23
- package/dist/capture.js.map +1 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +234 -13
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +21 -0
- package/dist/config.js.map +1 -1
- package/dist/connectors/github/ingest.d.ts.map +1 -1
- package/dist/connectors/github/ingest.js +17 -0
- package/dist/connectors/github/ingest.js.map +1 -1
- package/dist/connectors/slack/ingest.d.ts.map +1 -1
- package/dist/connectors/slack/ingest.js +15 -0
- package/dist/connectors/slack/ingest.js.map +1 -1
- package/dist/consolidate.d.ts.map +1 -1
- package/dist/consolidate.js +623 -312
- package/dist/consolidate.js.map +1 -1
- package/dist/dag.d.ts +10 -0
- package/dist/dag.d.ts.map +1 -1
- package/dist/dag.js +32 -3
- package/dist/dag.js.map +1 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +43 -1
- package/dist/db.js.map +1 -1
- package/dist/extract.d.ts.map +1 -1
- package/dist/extract.js +18 -1
- package/dist/extract.js.map +1 -1
- package/dist/importers.d.ts +10 -0
- package/dist/importers.d.ts.map +1 -1
- package/dist/importers.js +242 -159
- package/dist/importers.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +41 -5
- package/dist/mcp/server.js.map +1 -1
- package/dist/memory-value-weights.d.ts +22 -0
- package/dist/memory-value-weights.d.ts.map +1 -0
- package/dist/memory-value-weights.js +31 -0
- package/dist/memory-value-weights.js.map +1 -0
- package/dist/memory-value.d.ts +123 -0
- package/dist/memory-value.d.ts.map +1 -0
- package/dist/memory-value.js +255 -0
- package/dist/memory-value.js.map +1 -0
- package/dist/reject-flow.d.ts +66 -0
- package/dist/reject-flow.d.ts.map +1 -0
- package/dist/reject-flow.js +207 -0
- package/dist/reject-flow.js.map +1 -0
- package/dist/rejection.d.ts +100 -0
- package/dist/rejection.d.ts.map +1 -0
- package/dist/rejection.js +157 -0
- package/dist/rejection.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +5 -0
- package/dist/server.js.map +1 -1
- package/dist/shared.d.ts +9 -0
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +57 -6
- package/dist/shared.js.map +1 -1
- package/dist/sleep-redact.d.ts +2 -0
- package/dist/sleep-redact.d.ts.map +1 -1
- package/dist/sleep-redact.js +2 -0
- package/dist/sleep-redact.js.map +1 -1
- package/dist/src/api.js +78 -2
- package/dist/src/api.js.map +1 -1
- package/dist/src/audit.js.map +1 -1
- package/dist/src/capture.js +79 -23
- package/dist/src/capture.js.map +1 -1
- package/dist/src/cli.js +234 -13
- package/dist/src/cli.js.map +1 -1
- package/dist/src/config.js +21 -0
- package/dist/src/config.js.map +1 -1
- package/dist/src/connectors/github/ingest.js +17 -0
- package/dist/src/connectors/github/ingest.js.map +1 -1
- package/dist/src/connectors/slack/ingest.js +15 -0
- package/dist/src/connectors/slack/ingest.js.map +1 -1
- package/dist/src/consolidate.js +623 -312
- package/dist/src/consolidate.js.map +1 -1
- package/dist/src/dag.js +32 -3
- package/dist/src/dag.js.map +1 -1
- package/dist/src/db.js +43 -1
- package/dist/src/db.js.map +1 -1
- package/dist/src/extract.js +18 -1
- package/dist/src/extract.js.map +1 -1
- package/dist/src/importers.js +242 -159
- package/dist/src/importers.js.map +1 -1
- package/dist/src/mcp/server.js +41 -5
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/memory-value-weights.js +31 -0
- package/dist/src/memory-value-weights.js.map +1 -0
- package/dist/src/memory-value.js +255 -0
- package/dist/src/memory-value.js.map +1 -0
- package/dist/src/reject-flow.js +207 -0
- package/dist/src/reject-flow.js.map +1 -0
- package/dist/src/rejection.js +157 -0
- package/dist/src/rejection.js.map +1 -0
- package/dist/src/server.js +5 -0
- package/dist/src/server.js.map +1 -1
- package/dist/src/shared.js +57 -6
- package/dist/src/shared.js.map +1 -1
- package/dist/src/sleep-redact.js +2 -0
- package/dist/src/sleep-redact.js.map +1 -1
- package/dist/src/store.js +498 -35
- package/dist/src/store.js.map +1 -1
- package/dist/src/version.js +1 -1
- package/dist/store.d.ts +107 -3
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +498 -35
- package/dist/store.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LC2-E3 — learned memory-value scorer, wired into the sleep decay pass as a
|
|
3
|
+
* rescue-only veto (design D1/D2, docs/plans/2026-08-10-lc2-e3-mv-wiring.md).
|
|
4
|
+
*
|
|
5
|
+
* computeMvFeatures mirrors benchmarks/memory-value/extract.mjs's
|
|
6
|
+
* computeFeatures for the 8 live dims the E2 fitter optimized over
|
|
7
|
+
* (FIT_DIMS) — the only dims MEMORY_VALUE_WEIGHTS carries a weight for.
|
|
8
|
+
* Any future edit to either side must keep them byte-equivalent; the parity
|
|
9
|
+
* test in tests/memory-value-wiring.test.ts enforces this.
|
|
10
|
+
*
|
|
11
|
+
* scoreEntries mirrors benchmarks/memory-value/evaluate.mjs's per-store
|
|
12
|
+
* min-max normalization + weighted scorer (no additional orientation
|
|
13
|
+
* multiply — the frozen weights already encode sign/orientation).
|
|
14
|
+
*
|
|
15
|
+
* rescueSet implements D1's rescue-only semantics: a condemned entry is
|
|
16
|
+
* rescued iff its learned score ranks in the top 30% (RESCUE_BUDGET, the E2
|
|
17
|
+
* keep-budget operating point) of its own tenant's non-pinned candidate set
|
|
18
|
+
* (D2). Deletes(flag-on) subset Deletes(flag-off) by construction — this
|
|
19
|
+
* function can only ever shrink the condemned set, never grow it.
|
|
20
|
+
*/
|
|
21
|
+
import { calculateStrength } from './memory.js';
|
|
22
|
+
import { compareEntryIdentity } from './compare.js';
|
|
23
|
+
import { MEMORY_VALUE_WEIGHTS, SOURCE_ARTIFACT_SHA256 } from './memory-value-weights.js';
|
|
24
|
+
/** The 8 live feature dims (FIT_DIMS) — canonical order for iteration. */
|
|
25
|
+
export const MV_FEATURE_NAMES = [
|
|
26
|
+
'age_days',
|
|
27
|
+
'half_life_days',
|
|
28
|
+
'strength',
|
|
29
|
+
'retrieval_count',
|
|
30
|
+
'outcome_positive',
|
|
31
|
+
'outcome_negative',
|
|
32
|
+
'outcome_ratio',
|
|
33
|
+
'content_length',
|
|
34
|
+
];
|
|
35
|
+
/** The E2 keep-budget operating point (the only point with measured
|
|
36
|
+
* evidence) — a code constant tied to that evidence, not user-tunable. */
|
|
37
|
+
const RESCUE_BUDGET = 0.3;
|
|
38
|
+
/**
|
|
39
|
+
* Review-round F1 (small-tenant degeneracy): below this per-tenant
|
|
40
|
+
* non-pinned candidate-set size, a rank statistic is noise — E2's evidence
|
|
41
|
+
* says nothing about tiny scale — and the floor prevents immortal-entry
|
|
42
|
+
* convergence: keepN=ceil(0.3*N) guarantees >=1 rescue at N=1, so without a
|
|
43
|
+
* floor a condemned-only 1-entry tenant would be rescued every single sleep
|
|
44
|
+
* forever. A condemned-only tenant below the floor instead drains normally
|
|
45
|
+
* as entries are deleted: the surviving rescued subset only ever shrinks
|
|
46
|
+
* toward 0, never regrows past 10 to regain eligibility on its own. Code
|
|
47
|
+
* constant tied to that reasoning, not user-tunable (same posture as
|
|
48
|
+
* RESCUE_BUDGET).
|
|
49
|
+
*/
|
|
50
|
+
const MIN_RESCUE_GROUP = 10;
|
|
51
|
+
/**
|
|
52
|
+
* Blind v1 feature dict for one MemoryEntry, restricted to the 8 dims the
|
|
53
|
+
* frozen weights carry (mirrors extract.mjs's computeFeatures).
|
|
54
|
+
*
|
|
55
|
+
* CRITICAL: `strength` is CLOCK-BASIS `calculateStrength(entry, now)` with
|
|
56
|
+
* NO DecayOptions — that is how the frozen weights' training features were
|
|
57
|
+
* computed (extract.mjs never passes decayOpts). Passing the production
|
|
58
|
+
* decay basis (config.decayBasis via consolidate.ts's decayOpts) into this
|
|
59
|
+
* feature would silently break parity with the frozen weight vector. This
|
|
60
|
+
* is an intentional divergence from the condemnation TRIGGER in
|
|
61
|
+
* consolidate.ts, which keeps using decayOpts as today — only the rescue
|
|
62
|
+
* FEATURE is clock-basis.
|
|
63
|
+
*/
|
|
64
|
+
export function computeMvFeatures(entry, now) {
|
|
65
|
+
const ageDays = (now.getTime() - Date.parse(entry.created)) / (1000 * 60 * 60 * 24);
|
|
66
|
+
const pos = entry.outcome_positive ?? 0;
|
|
67
|
+
const neg = entry.outcome_negative ?? 0;
|
|
68
|
+
return {
|
|
69
|
+
age_days: ageDays,
|
|
70
|
+
half_life_days: entry.half_life_days,
|
|
71
|
+
strength: calculateStrength(entry, now), // clock-basis, never decayOpts — see doc comment above
|
|
72
|
+
retrieval_count: entry.retrieval_count,
|
|
73
|
+
outcome_positive: pos,
|
|
74
|
+
outcome_negative: neg,
|
|
75
|
+
outcome_ratio: (pos - neg) / (pos + neg + 1), // same formula as extract.mjs / calculateRewardFactor's ratio term
|
|
76
|
+
content_length: entry.content.length,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Throws if the weight constant is malformed: fewer/extra dims, a
|
|
81
|
+
* non-finite weight value, or a missing source digest. Flag-on + a broken
|
|
82
|
+
* constant must THROW, never silently behave as flag-off.
|
|
83
|
+
*
|
|
84
|
+
* Parameterized (defaults to the real frozen singleton) so the throw
|
|
85
|
+
* conditions are directly unit-testable without mutating the frozen
|
|
86
|
+
* MEMORY_VALUE_WEIGHTS export — mirrors fit.mjs's verifyFrozenWeights
|
|
87
|
+
* (see tests/memory-value-fit.test.ts's `describe('verifyFrozenWeights')`).
|
|
88
|
+
*/
|
|
89
|
+
export function validateWeights(weights = MEMORY_VALUE_WEIGHTS, digest = SOURCE_ARTIFACT_SHA256) {
|
|
90
|
+
for (const key of MV_FEATURE_NAMES) {
|
|
91
|
+
const v = weights[key];
|
|
92
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) {
|
|
93
|
+
throw new Error(`memory-value: weights constant is malformed — "${key}" is not a finite number ` +
|
|
94
|
+
`(src/memory-value-weights.ts)`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (typeof digest !== 'string' || digest.length === 0) {
|
|
98
|
+
throw new Error('memory-value: weights constant is malformed — SOURCE_ARTIFACT_SHA256 digest missing (src/memory-value-weights.ts)');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Min-max normalize each of the 8 features over the given entry set
|
|
103
|
+
* (constant feature -> 0, matching evaluate.mjs), then score = dot(weights,
|
|
104
|
+
* normalized). The normalization context is exactly the entries passed in —
|
|
105
|
+
* callers control the bounded scope (D2: per-tenant, non-pinned).
|
|
106
|
+
*
|
|
107
|
+
* `weights` defaults to the real frozen singleton; parameterized (like
|
|
108
|
+
* validateWeights) so callers/tests can score against an explicit vector
|
|
109
|
+
* without touching the module singleton.
|
|
110
|
+
*
|
|
111
|
+
* Review-round F2 (non-finite features): Date.parse on a malformed `created`
|
|
112
|
+
* string yields NaN, and NaN would silently corrupt every OTHER entry's
|
|
113
|
+
* min-max in the same group. An entry with ANY non-finite computed feature
|
|
114
|
+
* is excluded from the normalization context entirely (its raw values never
|
|
115
|
+
* touch min/max) and always scores -Infinity — the lowest possible score, so
|
|
116
|
+
* it sorts to the bottom of its tenant deterministically and (via rescueSet's
|
|
117
|
+
* explicit finite-score guard below) can never be rescued. Conservative
|
|
118
|
+
* direction: deletes(flag-on) subset deletes(flag-off) still holds.
|
|
119
|
+
*/
|
|
120
|
+
export function scoreEntries(entries, now, weights = MEMORY_VALUE_WEIGHTS) {
|
|
121
|
+
const raw = new Map();
|
|
122
|
+
for (const e of entries)
|
|
123
|
+
raw.set(e.id, computeMvFeatures(e, now));
|
|
124
|
+
const nonFiniteIds = new Set();
|
|
125
|
+
for (const e of entries) {
|
|
126
|
+
const nf = raw.get(e.id);
|
|
127
|
+
if (MV_FEATURE_NAMES.some((f) => !Number.isFinite(nf[f])))
|
|
128
|
+
nonFiniteIds.add(e.id);
|
|
129
|
+
}
|
|
130
|
+
const normContextEntries = entries.filter((e) => !nonFiniteIds.has(e.id));
|
|
131
|
+
const minMax = new Map();
|
|
132
|
+
for (const f of MV_FEATURE_NAMES) {
|
|
133
|
+
let min = Infinity;
|
|
134
|
+
let max = -Infinity;
|
|
135
|
+
for (const e of normContextEntries) {
|
|
136
|
+
const v = raw.get(e.id)[f];
|
|
137
|
+
if (v < min)
|
|
138
|
+
min = v;
|
|
139
|
+
if (v > max)
|
|
140
|
+
max = v;
|
|
141
|
+
}
|
|
142
|
+
minMax.set(f, { min, max });
|
|
143
|
+
}
|
|
144
|
+
const norm = (f, v) => {
|
|
145
|
+
const { min, max } = minMax.get(f);
|
|
146
|
+
return max === min ? 0 : (v - min) / (max - min);
|
|
147
|
+
};
|
|
148
|
+
const scores = new Map();
|
|
149
|
+
for (const e of entries) {
|
|
150
|
+
if (nonFiniteIds.has(e.id)) {
|
|
151
|
+
scores.set(e.id, -Infinity);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const nf = raw.get(e.id);
|
|
155
|
+
let sum = 0;
|
|
156
|
+
for (const f of MV_FEATURE_NAMES)
|
|
157
|
+
sum += weights[f] * norm(f, nf[f]);
|
|
158
|
+
scores.set(e.id, sum);
|
|
159
|
+
}
|
|
160
|
+
return scores;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Groups non-pinned entries by tenantId (D2), scores + ranks each tenant's
|
|
164
|
+
* group independently, and returns per-entry rank context for every
|
|
165
|
+
* non-pinned entry (not just condemned ones) — the shared basis for both
|
|
166
|
+
* rescueSet's rescue decision and consolidate.ts's audit-row rank context,
|
|
167
|
+
* so the two never compute the ranking differently.
|
|
168
|
+
*
|
|
169
|
+
* `weights`/`digest` default to the real frozen singleton — parameterized
|
|
170
|
+
* (like validateWeights/scoreEntries) purely for direct unit-testability of
|
|
171
|
+
* the fail-loud path, never overridden by production callers.
|
|
172
|
+
*/
|
|
173
|
+
export function rankNonPinnedByTenant(entries, now, weights = MEMORY_VALUE_WEIGHTS, digest = SOURCE_ARTIFACT_SHA256) {
|
|
174
|
+
validateWeights(weights, digest);
|
|
175
|
+
const byTenant = new Map();
|
|
176
|
+
for (const e of entries) {
|
|
177
|
+
if (e.pinned)
|
|
178
|
+
continue; // D2: pinned entries never compete for rescue (never condemned)
|
|
179
|
+
// F9: guard undefined tenantId the same way dag.ts:341 does — the
|
|
180
|
+
// MemoryEntry type says `string`, but a raw/legacy row can still carry
|
|
181
|
+
// undefined at runtime, and grouping it under the literal key
|
|
182
|
+
// "undefined" would silently split it into its own singleton tenant.
|
|
183
|
+
const tenantId = e.tenantId ?? 'default';
|
|
184
|
+
const list = byTenant.get(tenantId);
|
|
185
|
+
if (list)
|
|
186
|
+
list.push(e);
|
|
187
|
+
else
|
|
188
|
+
byTenant.set(tenantId, [e]);
|
|
189
|
+
}
|
|
190
|
+
const result = new Map();
|
|
191
|
+
for (const [tenantId, group] of byTenant) {
|
|
192
|
+
const scores = scoreEntries(group, now, weights);
|
|
193
|
+
// score DESC -> compareEntryIdentity (content asc -> id asc), the shared
|
|
194
|
+
// deterministic tie-break used by every score-primary sort site in this
|
|
195
|
+
// codebase (src/compare.ts). F2: `-Infinity - -Infinity` is NaN, not 0 —
|
|
196
|
+
// two non-finite-feature entries tied at -Infinity would otherwise fall
|
|
197
|
+
// through to `diff` (NaN), which Array.sort treats as "no preference"
|
|
198
|
+
// and leaves insertion-order-dependent. Route NaN through the same
|
|
199
|
+
// deterministic tie-break as an exact-zero diff.
|
|
200
|
+
const sorted = [...group].sort((a, b) => {
|
|
201
|
+
const diff = scores.get(b.id) - scores.get(a.id);
|
|
202
|
+
return diff === 0 || Number.isNaN(diff) ? compareEntryIdentity(a, b) : diff;
|
|
203
|
+
});
|
|
204
|
+
// F1: tenants smaller than MIN_RESCUE_GROUP never rescue (keepN 0) — see
|
|
205
|
+
// that constant's doc comment.
|
|
206
|
+
const keepN = sorted.length < MIN_RESCUE_GROUP
|
|
207
|
+
? 0
|
|
208
|
+
: Math.min(sorted.length, Math.ceil(RESCUE_BUDGET * sorted.length));
|
|
209
|
+
sorted.forEach((e, i) => {
|
|
210
|
+
result.set(e.id, {
|
|
211
|
+
tenantId,
|
|
212
|
+
score: scores.get(e.id),
|
|
213
|
+
rank: i + 1,
|
|
214
|
+
totalNonPinned: sorted.length,
|
|
215
|
+
keepN,
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* D1 rescue decision: a condemned entry is rescued iff it ranks in the top
|
|
223
|
+
* 30% of its tenant's non-pinned candidate set by learned score. Returns the
|
|
224
|
+
* subset of condemnedIds that are rescued — the caller filters commits
|
|
225
|
+
* (rescued -> survivors) and threads the same set into detectConflicts.
|
|
226
|
+
*
|
|
227
|
+
* `weights`/`digest` default to the real frozen singleton; production
|
|
228
|
+
* callers (consolidate.ts) never pass overrides — the params exist purely so
|
|
229
|
+
* "flag on + a broken constant throws" is directly testable end-to-end
|
|
230
|
+
* through this function without mutating the frozen module singleton.
|
|
231
|
+
*
|
|
232
|
+
* `precomputedRanks` (round-2 code-review P2-2): when the caller has already
|
|
233
|
+
* computed the per-tenant ranking (e.g. consolidate.ts needs it separately
|
|
234
|
+
* for detail/audit rank context), pass it here to skip the internal
|
|
235
|
+
* rankNonPinnedByTenant call — the whole-store ranking pass then runs
|
|
236
|
+
* exactly once per sleep instead of twice. Omitted (the default), rescueSet
|
|
237
|
+
* computes it internally as before — existing callers/tests are unaffected.
|
|
238
|
+
*/
|
|
239
|
+
export function rescueSet(entries, condemnedIds, now, weights = MEMORY_VALUE_WEIGHTS, digest = SOURCE_ARTIFACT_SHA256, precomputedRanks) {
|
|
240
|
+
validateWeights(weights, digest); // fail loud before any rescue computation (constraint 5)
|
|
241
|
+
const ranked = precomputedRanks ?? rankNonPinnedByTenant(entries, now, weights, digest);
|
|
242
|
+
const rescued = new Set();
|
|
243
|
+
for (const id of condemnedIds) {
|
|
244
|
+
const info = ranked.get(id);
|
|
245
|
+
// F2: Number.isFinite(info.score) is an explicit, absolute guard — not
|
|
246
|
+
// just reliance on -Infinity naturally sorting last. In the degenerate
|
|
247
|
+
// case where every entry in a tenant is non-finite-scored (a tie at
|
|
248
|
+
// -Infinity), rank position alone could otherwise place one inside
|
|
249
|
+
// keepN; this makes "never rescued" hold regardless.
|
|
250
|
+
if (info && info.rank <= info.keepN && Number.isFinite(info.score))
|
|
251
|
+
rescued.add(id);
|
|
252
|
+
}
|
|
253
|
+
return rescued;
|
|
254
|
+
}
|
|
255
|
+
//# sourceMappingURL=memory-value.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-value.js","sourceRoot":"","sources":["../src/memory-value.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AT1 rejected-value tombstone — shared reject/unreject/list flow.
|
|
3
|
+
* docs/plans/2026-08-15-at1-rejected-value-tombstone.md (T2, plan §4).
|
|
4
|
+
*
|
|
5
|
+
* The CLI (`hippo reject`/`rejections`/`unreject`) and the Context-based
|
|
6
|
+
* `api.reject`/`api.unreject`/`api.listRejections` surfaces both need the
|
|
7
|
+
* SAME multi-step transaction + post-commit mirror-purge flow. Extracted
|
|
8
|
+
* here (leaf module) so neither duplicates it.
|
|
9
|
+
*
|
|
10
|
+
* Module direction: this file imports from store.ts, rejection.ts, and
|
|
11
|
+
* raw-archive.ts. Nothing imports FROM this file except cli.ts and api.ts,
|
|
12
|
+
* so it introduces no cycle.
|
|
13
|
+
*/
|
|
14
|
+
import { type RejectedValueRow } from './rejection.js';
|
|
15
|
+
export interface RejectFlowOpts {
|
|
16
|
+
hippoRoot: string;
|
|
17
|
+
tenantId: string;
|
|
18
|
+
actor: string;
|
|
19
|
+
reason: string;
|
|
20
|
+
/** By-id form: reject the CURRENT content of an existing memory. */
|
|
21
|
+
memoryId?: string;
|
|
22
|
+
/** Pre-emptive form: reject a value not currently stored (or already gone). */
|
|
23
|
+
value?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface RejectFlowResult {
|
|
26
|
+
digest: string;
|
|
27
|
+
/** The rejected content, for the CLI's at-reject-time echo (plan §2: the
|
|
28
|
+
* tombstone itself stores no content — this is the only place it's seen
|
|
29
|
+
* again after this call returns). */
|
|
30
|
+
content: string;
|
|
31
|
+
/** Every live row removed this call (all tenant rows whose normalized
|
|
32
|
+
* digest matched — not just the id passed, per the K1/R7 duplicate
|
|
33
|
+
* lesson). */
|
|
34
|
+
removedIds: string[];
|
|
35
|
+
/** Subset of removedIds that were kind='raw' (archived, not deleted). */
|
|
36
|
+
removedRawIds: string[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* `hippo reject` / `api.reject` core flow. ONE connection, one transaction:
|
|
40
|
+
* insert the tombstone, enumerate + remove every live tenant row whose
|
|
41
|
+
* normalized digest matches (kind-aware), one aggregate `reject_value`
|
|
42
|
+
* audit, COMMIT. Then post-commit (mirrors the existing purge+reaper
|
|
43
|
+
* pattern verbatim from api.archiveRaw, api.ts:1913-1938): best-effort
|
|
44
|
+
* mirror purge per removed id, `mirror_cleaned_at` stamps for raw ids, one
|
|
45
|
+
* index mirror rewrite.
|
|
46
|
+
*/
|
|
47
|
+
export declare function rejectValue(opts: RejectFlowOpts): RejectFlowResult;
|
|
48
|
+
export type UnrejectOutcome = {
|
|
49
|
+
status: 'ok';
|
|
50
|
+
digest: string;
|
|
51
|
+
reason: string | null;
|
|
52
|
+
} | {
|
|
53
|
+
status: 'not_found';
|
|
54
|
+
} | {
|
|
55
|
+
status: 'ambiguous';
|
|
56
|
+
candidates: RejectedValueRow[];
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* `hippo unreject` / `api.unreject` — resolve a unique tombstone by digest
|
|
60
|
+
* (or prefix), delete it, audit `unreject_value`. The only v1 escape hatch
|
|
61
|
+
* (plan §4): no per-write force flag.
|
|
62
|
+
*/
|
|
63
|
+
export declare function unrejectValue(hippoRoot: string, tenantId: string, digestOrPrefix: string, actor: string): UnrejectOutcome;
|
|
64
|
+
/** `hippo rejections` / `api.listRejections` — list tombstones for a tenant. */
|
|
65
|
+
export declare function listRejectionsForTenant(hippoRoot: string, tenantId: string): RejectedValueRow[];
|
|
66
|
+
//# sourceMappingURL=reject-flow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reject-flow.d.ts","sourceRoot":"","sources":["../src/reject-flow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAYH,OAAO,EAML,KAAK,gBAAgB,EACtB,MAAM,gBAAgB,CAAC;AAExB,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,oEAAoE;IACpE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf;;0CAEsC;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB;;mBAEe;IACf,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,yEAAyE;IACzE,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,cAAc,GAAG,gBAAgB,CAgIlE;AAED,MAAM,MAAM,eAAe,GACvB;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GACvD;IAAE,MAAM,EAAE,WAAW,CAAA;CAAE,GACvB;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,UAAU,EAAE,gBAAgB,EAAE,CAAA;CAAE,CAAC;AAE5D;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,MAAM,EACtB,KAAK,EAAE,MAAM,GACZ,eAAe,CAmCjB;AAED,gFAAgF;AAChF,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAQ/F"}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AT1 rejected-value tombstone — shared reject/unreject/list flow.
|
|
3
|
+
* docs/plans/2026-08-15-at1-rejected-value-tombstone.md (T2, plan §4).
|
|
4
|
+
*
|
|
5
|
+
* The CLI (`hippo reject`/`rejections`/`unreject`) and the Context-based
|
|
6
|
+
* `api.reject`/`api.unreject`/`api.listRejections` surfaces both need the
|
|
7
|
+
* SAME multi-step transaction + post-commit mirror-purge flow. Extracted
|
|
8
|
+
* here (leaf module) so neither duplicates it.
|
|
9
|
+
*
|
|
10
|
+
* Module direction: this file imports from store.ts, rejection.ts, and
|
|
11
|
+
* raw-archive.ts. Nothing imports FROM this file except cli.ts and api.ts,
|
|
12
|
+
* so it introduces no cycle.
|
|
13
|
+
*/
|
|
14
|
+
import { openHippoDb, closeHippoDb } from './db.js';
|
|
15
|
+
import { appendAuditEvent } from './audit.js';
|
|
16
|
+
import { archiveRawMemory } from './raw-archive.js';
|
|
17
|
+
import { initStore, deleteEntryCore, purgeMirrorBestEffort, writeIndexMirror, buildIndexFromDb, } from './store.js';
|
|
18
|
+
import { rejectionDigest, normalizeValueForRejection, insertRejectedValue, deleteRejectedValue, listRejectedValues, } from './rejection.js';
|
|
19
|
+
/**
|
|
20
|
+
* `hippo reject` / `api.reject` core flow. ONE connection, one transaction:
|
|
21
|
+
* insert the tombstone, enumerate + remove every live tenant row whose
|
|
22
|
+
* normalized digest matches (kind-aware), one aggregate `reject_value`
|
|
23
|
+
* audit, COMMIT. Then post-commit (mirrors the existing purge+reaper
|
|
24
|
+
* pattern verbatim from api.archiveRaw, api.ts:1913-1938): best-effort
|
|
25
|
+
* mirror purge per removed id, `mirror_cleaned_at` stamps for raw ids, one
|
|
26
|
+
* index mirror rewrite.
|
|
27
|
+
*/
|
|
28
|
+
export function rejectValue(opts) {
|
|
29
|
+
if (!opts.reason.trim()) {
|
|
30
|
+
throw new Error('reject requires a non-empty --reason (the tombstone stores no content; reason is its only identity).');
|
|
31
|
+
}
|
|
32
|
+
if (opts.memoryId === undefined && opts.value === undefined) {
|
|
33
|
+
throw new Error('reject requires either a memory id or --value.');
|
|
34
|
+
}
|
|
35
|
+
if (opts.memoryId !== undefined && opts.value !== undefined) {
|
|
36
|
+
// P2 fix: the CLI's flag parser already refuses both forms together;
|
|
37
|
+
// the shared flow itself didn't enforce it, so a direct api caller
|
|
38
|
+
// passing both silently got the memoryId path with `value` ignored —
|
|
39
|
+
// surprising for a caller who thought they were rejecting `value`.
|
|
40
|
+
throw new Error('reject accepts either a memory id or --value, not both.');
|
|
41
|
+
}
|
|
42
|
+
if (opts.value !== undefined && normalizeValueForRejection(opts.value).length === 0) {
|
|
43
|
+
// Direct api callers can pass strings the CLI flag parser would have
|
|
44
|
+
// refused; an empty-normalized tombstone would refuse nothing meaningful
|
|
45
|
+
// and pollute the listing.
|
|
46
|
+
throw new Error('reject --value requires non-empty content.');
|
|
47
|
+
}
|
|
48
|
+
initStore(opts.hippoRoot);
|
|
49
|
+
const db = openHippoDb(opts.hippoRoot);
|
|
50
|
+
try {
|
|
51
|
+
let content;
|
|
52
|
+
if (opts.memoryId !== undefined) {
|
|
53
|
+
const row = db
|
|
54
|
+
.prepare(`SELECT content, tenant_id FROM memories WHERE id = ?`)
|
|
55
|
+
.get(opts.memoryId);
|
|
56
|
+
if (!row || row.tenant_id !== opts.tenantId) {
|
|
57
|
+
throw new Error(`memory not found: ${opts.memoryId}`);
|
|
58
|
+
}
|
|
59
|
+
content = row.content;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
content = opts.value;
|
|
63
|
+
}
|
|
64
|
+
const digest = rejectionDigest(content);
|
|
65
|
+
const now = new Date().toISOString();
|
|
66
|
+
const removedIds = [];
|
|
67
|
+
const removedRawIds = [];
|
|
68
|
+
db.exec('BEGIN');
|
|
69
|
+
try {
|
|
70
|
+
insertRejectedValue(db, {
|
|
71
|
+
tenantId: opts.tenantId,
|
|
72
|
+
digest,
|
|
73
|
+
reason: opts.reason,
|
|
74
|
+
rejectedBy: opts.actor,
|
|
75
|
+
rejectedAt: now,
|
|
76
|
+
sourceMemoryId: opts.memoryId ?? null,
|
|
77
|
+
normalizedChars: normalizeValueForRejection(content).length,
|
|
78
|
+
});
|
|
79
|
+
// O(N) scan over the tenant's rows (plan §4): human-triggered command
|
|
80
|
+
// on ~1-5k-row stores — acceptable, documented. A digest column on
|
|
81
|
+
// memories is the escape if stores grow 100x; not needed now.
|
|
82
|
+
const rows = db
|
|
83
|
+
.prepare(`SELECT id, kind, content FROM memories WHERE tenant_id = ?`)
|
|
84
|
+
.all(opts.tenantId);
|
|
85
|
+
for (const row of rows) {
|
|
86
|
+
if (rejectionDigest(row.content) !== digest)
|
|
87
|
+
continue;
|
|
88
|
+
if (row.kind === 'raw') {
|
|
89
|
+
// Append-only trigger respected — archiveRawMemory is the only
|
|
90
|
+
// legitimate removal path for kind='raw', and its inner SAVEPOINT
|
|
91
|
+
// composes safely inside this BEGIN/COMMIT.
|
|
92
|
+
archiveRawMemory(db, row.id, { reason: opts.reason, who: opts.actor });
|
|
93
|
+
removedRawIds.push(row.id);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
// suppressForgetAudit: the aggregate reject_value row below is the
|
|
97
|
+
// trail for these removals, not N individual forget rows (plan
|
|
98
|
+
// §4, round-3 advisory 2 — mirrors api.ts:1873-1877).
|
|
99
|
+
deleteEntryCore(db, row.id, { actor: opts.actor, suppressForgetAudit: true });
|
|
100
|
+
}
|
|
101
|
+
removedIds.push(row.id);
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
appendAuditEvent(db, {
|
|
105
|
+
tenantId: opts.tenantId,
|
|
106
|
+
actor: opts.actor,
|
|
107
|
+
op: 'reject_value',
|
|
108
|
+
targetId: opts.memoryId,
|
|
109
|
+
metadata: { digest, removedIds, count: removedIds.length },
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// Best-effort — mirrors store.ts's private audit() semantics. This
|
|
114
|
+
// runs INSIDE the still-open transaction (COMMIT is the next
|
|
115
|
+
// statement): a swallowed audit failure lets the tombstone +
|
|
116
|
+
// removals commit without the trail row, rather than rolling the
|
|
117
|
+
// whole reject back over bookkeeping.
|
|
118
|
+
}
|
|
119
|
+
db.exec('COMMIT');
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
try {
|
|
123
|
+
db.exec('ROLLBACK');
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// already rolled back
|
|
127
|
+
}
|
|
128
|
+
throw err;
|
|
129
|
+
}
|
|
130
|
+
// Post-commit, db handle still open (same pattern as api.archiveRaw):
|
|
131
|
+
// best-effort mirror purge per removed id, reaper-backstop stamp for
|
|
132
|
+
// raw ids, one index mirror rewrite.
|
|
133
|
+
for (const id of removedIds) {
|
|
134
|
+
// AT1 fix: purgeMirrorBestEffort retries once, then — for non-raw ids,
|
|
135
|
+
// which cleanupArchivedMirrors' reaper never scans — reports the
|
|
136
|
+
// EXPLICIT leftover path(s) instead of the false "will retry via
|
|
137
|
+
// reaper" claim. See its own doc comment (store.ts, near
|
|
138
|
+
// removeEntryMirrors) for the full rationale.
|
|
139
|
+
const mirrorOk = purgeMirrorBestEffort(opts.hippoRoot, id, removedRawIds.includes(id), 'hippo reject');
|
|
140
|
+
if (mirrorOk && removedRawIds.includes(id)) {
|
|
141
|
+
db.prepare(`UPDATE raw_archive SET mirror_cleaned_at = ? WHERE memory_id = ?`).run(new Date().toISOString(), id);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (removedIds.length > 0) {
|
|
145
|
+
writeIndexMirror(opts.hippoRoot, buildIndexFromDb(db));
|
|
146
|
+
}
|
|
147
|
+
return { digest, content, removedIds, removedRawIds };
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
closeHippoDb(db);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* `hippo unreject` / `api.unreject` — resolve a unique tombstone by digest
|
|
155
|
+
* (or prefix), delete it, audit `unreject_value`. The only v1 escape hatch
|
|
156
|
+
* (plan §4): no per-write force flag.
|
|
157
|
+
*/
|
|
158
|
+
export function unrejectValue(hippoRoot, tenantId, digestOrPrefix, actor) {
|
|
159
|
+
// P2 fix: an empty/blank prefix startsWith-matches EVERY digest (every
|
|
160
|
+
// string starts with ''), which would previously fall through to the
|
|
161
|
+
// ambiguous-candidates branch and list the whole tombstone set instead of
|
|
162
|
+
// failing loud on the actually-invalid input. Reject before the DB round
|
|
163
|
+
// trip.
|
|
164
|
+
if (digestOrPrefix.trim().length === 0) {
|
|
165
|
+
return { status: 'not_found' };
|
|
166
|
+
}
|
|
167
|
+
initStore(hippoRoot);
|
|
168
|
+
const db = openHippoDb(hippoRoot);
|
|
169
|
+
try {
|
|
170
|
+
const all = listRejectedValues(db, tenantId);
|
|
171
|
+
const matches = all.filter((r) => r.digest.startsWith(digestOrPrefix));
|
|
172
|
+
if (matches.length === 0)
|
|
173
|
+
return { status: 'not_found' };
|
|
174
|
+
if (matches.length > 1)
|
|
175
|
+
return { status: 'ambiguous', candidates: matches };
|
|
176
|
+
const target = matches[0];
|
|
177
|
+
deleteRejectedValue(db, tenantId, target.digest);
|
|
178
|
+
try {
|
|
179
|
+
appendAuditEvent(db, {
|
|
180
|
+
tenantId,
|
|
181
|
+
actor,
|
|
182
|
+
op: 'unreject_value',
|
|
183
|
+
targetId: target.sourceMemoryId ?? undefined,
|
|
184
|
+
metadata: { digest: target.digest, reason: target.reason },
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// Best-effort, same as reject's audit call above.
|
|
189
|
+
}
|
|
190
|
+
return { status: 'ok', digest: target.digest, reason: target.reason };
|
|
191
|
+
}
|
|
192
|
+
finally {
|
|
193
|
+
closeHippoDb(db);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/** `hippo rejections` / `api.listRejections` — list tombstones for a tenant. */
|
|
197
|
+
export function listRejectionsForTenant(hippoRoot, tenantId) {
|
|
198
|
+
initStore(hippoRoot);
|
|
199
|
+
const db = openHippoDb(hippoRoot);
|
|
200
|
+
try {
|
|
201
|
+
return listRejectedValues(db, tenantId);
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
closeHippoDb(db);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=reject-flow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reject-flow.js","sourceRoot":"","sources":["../src/reject-flow.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AT1 rejected-value tombstone — core invariant.
|
|
3
|
+
* docs/plans/2026-08-15-at1-rejected-value-tombstone.md
|
|
4
|
+
*
|
|
5
|
+
* Exact-normalized-value semantics: a human who rejects a fact can refuse
|
|
6
|
+
* byte-stable re-ingestion of the same value across remember/capture/import/
|
|
7
|
+
* sync surfaces. Paraphrase/semantic matching is explicitly out of scope
|
|
8
|
+
* (documented limitation, plan "Design").
|
|
9
|
+
*
|
|
10
|
+
* Kept db-agnostic (helpers take a `DatabaseSyncLike` handle) and does NOT
|
|
11
|
+
* import from store.ts — store.ts imports from here, and the reverse would
|
|
12
|
+
* be a cycle.
|
|
13
|
+
*/
|
|
14
|
+
import type { DatabaseSyncLike } from './db.js';
|
|
15
|
+
/**
|
|
16
|
+
* Normalize content for rejection-digest comparisons: Unicode NFC →
|
|
17
|
+
* lowercase → collapse whitespace runs to a single space → trim. No
|
|
18
|
+
* punctuation stripping — over-normalization creates false refusals, which
|
|
19
|
+
* are worse than misses (plan §1).
|
|
20
|
+
*/
|
|
21
|
+
export declare function normalizeValueForRejection(content: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Full sha256 hex (64 chars) of the normalized content. Reuses the strong-
|
|
24
|
+
* identity convention (importers.ts:814 content-hash tag), NOT the privacy-
|
|
25
|
+
* lossy 16-char convention (recall-trace query hashing) — a tombstone lookup
|
|
26
|
+
* key needs collision resistance, not redaction (plan §1).
|
|
27
|
+
*/
|
|
28
|
+
export declare function rejectionDigest(content: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Thrown by the write-path guard (checkRejectionGuard, called from
|
|
31
|
+
* upsertEntryRow) when an incoming write would introduce a value matching a
|
|
32
|
+
* tombstoned digest. Carries enough context for the transaction-owner catch
|
|
33
|
+
* blocks (writeEntry, api.supersede) to write a post-rollback
|
|
34
|
+
* `reject_refusal` audit row via `auditRejectionRefusal` (plan §3).
|
|
35
|
+
*/
|
|
36
|
+
export declare class RejectedValueError extends Error {
|
|
37
|
+
readonly digest: string;
|
|
38
|
+
readonly tenantId: string;
|
|
39
|
+
readonly entryId: string;
|
|
40
|
+
readonly reason: string | null;
|
|
41
|
+
readonly rejectedAt: string;
|
|
42
|
+
constructor(opts: {
|
|
43
|
+
digest: string;
|
|
44
|
+
tenantId: string;
|
|
45
|
+
entryId: string;
|
|
46
|
+
reason: string | null;
|
|
47
|
+
rejectedAt: string;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
export interface RejectedValueRow {
|
|
51
|
+
tenantId: string;
|
|
52
|
+
digest: string;
|
|
53
|
+
reason: string | null;
|
|
54
|
+
rejectedBy: string | null;
|
|
55
|
+
rejectedAt: string;
|
|
56
|
+
sourceMemoryId: string | null;
|
|
57
|
+
normalizedChars: number | null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Look up a tombstone by tenant + digest. One indexed point query — the
|
|
61
|
+
* guard's common-case cost, a miss ends the guard (plan §3).
|
|
62
|
+
*/
|
|
63
|
+
export declare function findRejectedValue(db: DatabaseSyncLike, tenantId: string, digest: string): RejectedValueRow | null;
|
|
64
|
+
/**
|
|
65
|
+
* Insert (or refresh) a tombstone row. Caller owns the transaction — used by
|
|
66
|
+
* the T2 `reject` verb and `resolveConflict`'s `rejectLoserValue` path.
|
|
67
|
+
*/
|
|
68
|
+
export declare function insertRejectedValue(db: DatabaseSyncLike, opts: {
|
|
69
|
+
tenantId: string;
|
|
70
|
+
digest: string;
|
|
71
|
+
reason: string;
|
|
72
|
+
rejectedBy: string;
|
|
73
|
+
rejectedAt: string;
|
|
74
|
+
sourceMemoryId?: string | null;
|
|
75
|
+
normalizedChars: number;
|
|
76
|
+
}): void;
|
|
77
|
+
/**
|
|
78
|
+
* Delete a tombstone by tenant + exact digest — the T2 `unreject` verb, the
|
|
79
|
+
* only v1 escape hatch (plan §4).
|
|
80
|
+
*/
|
|
81
|
+
export declare function deleteRejectedValue(db: DatabaseSyncLike, tenantId: string, digest: string): boolean;
|
|
82
|
+
/** List tombstones for a tenant, newest first — the T2 `rejections` verb. */
|
|
83
|
+
export declare function listRejectedValues(db: DatabaseSyncLike, tenantId: string): RejectedValueRow[];
|
|
84
|
+
/**
|
|
85
|
+
* The write-path guard's check helper, called from `upsertEntryRow`
|
|
86
|
+
* (store.ts). Fires when the incoming content's digest matches a tombstone
|
|
87
|
+
* AND the write *introduces* that content: the row is new, OR the stored
|
|
88
|
+
* row's content digest differs from the incoming one — an UPSERT editing a
|
|
89
|
+
* same-id row TO a rejected value is a content introduction and must be
|
|
90
|
+
* refused. Unchanged same-id re-persists (recall boost, decay, star toggle)
|
|
91
|
+
* are exempt by construction (plan §3).
|
|
92
|
+
*
|
|
93
|
+
* Ordering minimizes queries on the common (miss) path: (a) caller has
|
|
94
|
+
* already computed nothing yet — this does the digest + point lookup first;
|
|
95
|
+
* (b) a miss returns immediately (ONE indexed point query); (c) only on a
|
|
96
|
+
* tombstone hit does it SELECT the stored row's content to classify
|
|
97
|
+
* new-row vs content-introduction (+1 query, rare path).
|
|
98
|
+
*/
|
|
99
|
+
export declare function checkRejectionGuard(db: DatabaseSyncLike, tenantId: string, entryId: string, content: string): void;
|
|
100
|
+
//# sourceMappingURL=rejection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rejection.d.ts","sourceRoot":"","sources":["../src/rejection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEhD;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAElE;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED;;;;;;GAMG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;gBAEhB,IAAI,EAAE;QAChB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,UAAU,EAAE,MAAM,CAAC;KACpB;CAYF;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAwBD;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,EAAE,EAAE,gBAAgB,EACpB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,gBAAgB,GAAG,IAAI,CAQzB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,EAAE,EAAE,gBAAgB,EACpB,IAAI,EAAE;IACJ,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,eAAe,EAAE,MAAM,CAAC;CACzB,GACA,IAAI,CAmBN;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAGnG;AAED,6EAA6E;AAC7E,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAQ7F;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,CACjC,EAAE,EAAE,gBAAgB,EACpB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,GACd,IAAI,CAmCN"}
|