@gmickel/gno 1.20.0 → 1.21.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.
Files changed (49) hide show
  1. package/README.md +19 -4
  2. package/assets/skill/SKILL.md +46 -15
  3. package/package.json +1 -1
  4. package/spec/cli.md +100 -0
  5. package/spec/db/schema.sql +170 -0
  6. package/spec/mcp.md +22 -0
  7. package/spec/output-schemas/capsule-reverified-event.schema.json +47 -0
  8. package/spec/output-schemas/changes.schema.json +280 -0
  9. package/spec/output-schemas/document-diff.schema.json +185 -0
  10. package/spec/output-schemas/impact.schema.json +122 -0
  11. package/spec/output-schemas/saved-capsule-list.schema.json +16 -0
  12. package/spec/output-schemas/saved-capsule-registration.schema.json +172 -0
  13. package/spec/output-schemas/saved-capsule-reverification.schema.json +59 -0
  14. package/spec/output-schemas/saved-capsule-unwatch.schema.json +16 -0
  15. package/spec/output-schemas/saved-capsule-watch.schema.json +17 -0
  16. package/src/cli/commands/changes.ts +160 -0
  17. package/src/cli/commands/context-saved.ts +189 -0
  18. package/src/cli/options.ts +8 -0
  19. package/src/cli/program.ts +195 -0
  20. package/src/core/capsule-registry.ts +279 -0
  21. package/src/core/capsule-reverification-scheduler.ts +218 -0
  22. package/src/core/capsule-reverification.ts +289 -0
  23. package/src/core/change-diff.ts +182 -0
  24. package/src/core/change-journal.ts +228 -0
  25. package/src/core/knowledge-delta.ts +395 -0
  26. package/src/core/knowledge-impact.ts +202 -0
  27. package/src/ingestion/sync.ts +214 -165
  28. package/src/mcp/tools/changes.ts +80 -0
  29. package/src/mcp/tools/index.ts +29 -0
  30. package/src/sdk/client.ts +42 -0
  31. package/src/sdk/index.ts +7 -0
  32. package/src/sdk/types.ts +22 -0
  33. package/src/serve/doc-events.ts +12 -1
  34. package/src/serve/resident-runtime.ts +22 -0
  35. package/src/serve/routes/api.ts +13 -0
  36. package/src/serve/routes/changes.ts +102 -0
  37. package/src/serve/server.ts +34 -0
  38. package/src/serve/watch-service.ts +9 -0
  39. package/src/store/index.ts +21 -0
  40. package/src/store/migrations/015-document-change-journal.ts +85 -0
  41. package/src/store/migrations/016-saved-capsules.ts +131 -0
  42. package/src/store/migrations/017-document-change-retention-counters.ts +33 -0
  43. package/src/store/migrations/018-saved-capsule-registration-epoch.ts +24 -0
  44. package/src/store/migrations/019-saved-capsule-registration-generation.ts +53 -0
  45. package/src/store/migrations/index.ts +10 -0
  46. package/src/store/sqlite/adapter.ts +291 -7
  47. package/src/store/sqlite/capsule-registry-store.ts +534 -0
  48. package/src/store/sqlite/change-journal-store.ts +473 -0
  49. package/src/store/types.ts +262 -0
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Shared, storage-agnostic contracts for the document change journal.
3
+ */
4
+
5
+ import type {
6
+ DocumentChangeDateDelta,
7
+ DocumentChangeSet,
8
+ DocumentChangeStructureDelta,
9
+ DocumentChangeRetentionPolicy,
10
+ } from "../store/types";
11
+
12
+ const CURSOR_PREFIX = "gno-change-v1.";
13
+ const MAX_DELTA_ITEMS_PER_SIDE = 16;
14
+ const MAX_DELTA_VALUE_JSON_BYTES = 256;
15
+ export const MAX_DOCUMENT_CHANGE_DELTA_JSON_BYTES = 16 * 1024;
16
+ const UTF8_ENCODER = new TextEncoder();
17
+
18
+ export const DEFAULT_DOCUMENT_CHANGE_RETENTION: DocumentChangeRetentionPolicy =
19
+ {
20
+ maxAgeDays: 30,
21
+ maxEntries: 10_000,
22
+ maxBytes: 16 * 1024 * 1024,
23
+ };
24
+
25
+ export const EMPTY_DOCUMENT_CHANGE_SET: DocumentChangeSet = {
26
+ added: [],
27
+ removed: [],
28
+ };
29
+
30
+ export const EMPTY_DOCUMENT_CHANGE_DATE_DELTA: DocumentChangeDateDelta = {
31
+ added: [],
32
+ removed: [],
33
+ changed: [],
34
+ };
35
+
36
+ export const EMPTY_DOCUMENT_CHANGE_STRUCTURE_DELTA: DocumentChangeStructureDelta =
37
+ {
38
+ headings: EMPTY_DOCUMENT_CHANGE_SET,
39
+ links: EMPTY_DOCUMENT_CHANGE_SET,
40
+ typedEdges: EMPTY_DOCUMENT_CHANGE_SET,
41
+ dates: EMPTY_DOCUMENT_CHANGE_DATE_DELTA,
42
+ truncated: false,
43
+ };
44
+
45
+ const normalizeValues = (
46
+ values: readonly string[] | undefined
47
+ ): { values: string[]; truncated: boolean } => {
48
+ const unique = [
49
+ ...new Set(
50
+ (values ?? [])
51
+ .map((value) => value.trim())
52
+ .filter((value) => value.length > 0)
53
+ ),
54
+ ].sort();
55
+ const selected: string[] = [];
56
+ const selectedValues = new Set<string>();
57
+ let truncated = false;
58
+ for (const value of unique) {
59
+ let normalized = "";
60
+ let jsonByteLength = 0;
61
+ for (const character of value) {
62
+ const escaped = JSON.stringify(character).slice(1, -1);
63
+ const characterBytes = UTF8_ENCODER.encode(escaped).byteLength;
64
+ if (jsonByteLength + characterBytes > MAX_DELTA_VALUE_JSON_BYTES) {
65
+ truncated = true;
66
+ break;
67
+ }
68
+ normalized += character;
69
+ jsonByteLength += characterBytes;
70
+ }
71
+ if (normalized !== value) {
72
+ truncated = true;
73
+ }
74
+ if (selectedValues.has(normalized)) {
75
+ truncated = true;
76
+ continue;
77
+ }
78
+ if (selected.length === MAX_DELTA_ITEMS_PER_SIDE) {
79
+ truncated = true;
80
+ continue;
81
+ }
82
+ selected.push(normalized);
83
+ selectedValues.add(normalized);
84
+ }
85
+ return {
86
+ values: selected,
87
+ truncated,
88
+ };
89
+ };
90
+
91
+ export interface SerializedDocumentChangeStructureDelta {
92
+ delta: DocumentChangeStructureDelta;
93
+ headingDeltaJson: string;
94
+ linkDeltaJson: string;
95
+ typedEdgeDeltaJson: string;
96
+ dateDeltaJson: string;
97
+ }
98
+
99
+ const normalizeSet = (
100
+ value: Partial<DocumentChangeSet> | undefined
101
+ ): { value: DocumentChangeSet; truncated: boolean } => {
102
+ const added = normalizeValues(value?.added);
103
+ const removed = normalizeValues(value?.removed);
104
+ return {
105
+ value: { added: added.values, removed: removed.values },
106
+ truncated: added.truncated || removed.truncated,
107
+ };
108
+ };
109
+
110
+ const normalizeDates = (
111
+ value: Partial<DocumentChangeDateDelta> | undefined
112
+ ): { value: DocumentChangeDateDelta; truncated: boolean } => {
113
+ const added = normalizeValues(value?.added);
114
+ const removed = normalizeValues(value?.removed);
115
+ const changed = normalizeValues(value?.changed);
116
+ return {
117
+ value: {
118
+ added: added.values,
119
+ removed: removed.values,
120
+ changed: changed.values,
121
+ },
122
+ truncated: added.truncated || removed.truncated || changed.truncated,
123
+ };
124
+ };
125
+
126
+ export const normalizeDocumentChangeStructureDelta = (
127
+ value?: Partial<DocumentChangeStructureDelta>
128
+ ): DocumentChangeStructureDelta => {
129
+ const headings = normalizeSet(value?.headings);
130
+ const links = normalizeSet(value?.links);
131
+ const typedEdges = normalizeSet(value?.typedEdges);
132
+ const dates = normalizeDates(value?.dates);
133
+ return {
134
+ headings: headings.value,
135
+ links: links.value,
136
+ typedEdges: typedEdges.value,
137
+ dates: dates.value,
138
+ truncated:
139
+ (value?.truncated ?? false) ||
140
+ headings.truncated ||
141
+ links.truncated ||
142
+ typedEdges.truncated ||
143
+ dates.truncated,
144
+ };
145
+ };
146
+
147
+ /**
148
+ * Canonical storage projection for migration 015's UTF-8 byte constraints.
149
+ * Callers must use this instead of independently stringifying normalized deltas.
150
+ */
151
+ export const serializeDocumentChangeStructureDelta = (
152
+ value?: Partial<DocumentChangeStructureDelta>
153
+ ): SerializedDocumentChangeStructureDelta => {
154
+ const delta = normalizeDocumentChangeStructureDelta(value);
155
+ const serialized = {
156
+ headingDeltaJson: JSON.stringify(delta.headings),
157
+ linkDeltaJson: JSON.stringify(delta.links),
158
+ typedEdgeDeltaJson: JSON.stringify(delta.typedEdges),
159
+ dateDeltaJson: JSON.stringify(delta.dates),
160
+ };
161
+ for (const json of Object.values(serialized)) {
162
+ if (
163
+ UTF8_ENCODER.encode(json).byteLength >
164
+ MAX_DOCUMENT_CHANGE_DELTA_JSON_BYTES
165
+ ) {
166
+ throw new RangeError(
167
+ "Normalized document change structure exceeds its UTF-8 storage limit"
168
+ );
169
+ }
170
+ }
171
+ return { delta, ...serialized };
172
+ };
173
+
174
+ export const encodeDocumentChangeCursor = (sequence: number): string => {
175
+ if (!Number.isSafeInteger(sequence) || sequence < 0) {
176
+ throw new RangeError(
177
+ "Document change cursor sequence must be non-negative"
178
+ );
179
+ }
180
+ return `${CURSOR_PREFIX}${btoa(JSON.stringify({ sequence }))}`;
181
+ };
182
+
183
+ export const decodeDocumentChangeCursor = (cursor: string): number => {
184
+ if (!cursor.startsWith(CURSOR_PREFIX)) {
185
+ throw new TypeError("Invalid document change cursor");
186
+ }
187
+ try {
188
+ const parsed: unknown = JSON.parse(
189
+ atob(cursor.slice(CURSOR_PREFIX.length))
190
+ );
191
+ if (
192
+ !parsed ||
193
+ typeof parsed !== "object" ||
194
+ !("sequence" in parsed) ||
195
+ !Number.isSafeInteger(parsed.sequence) ||
196
+ (parsed.sequence as number) < 0
197
+ ) {
198
+ throw new TypeError("Invalid document change cursor");
199
+ }
200
+ return parsed.sequence as number;
201
+ } catch (cause) {
202
+ if (cause instanceof TypeError) {
203
+ throw cause;
204
+ }
205
+ throw new TypeError("Invalid document change cursor", { cause });
206
+ }
207
+ };
208
+
209
+ export const validateDocumentChangeRetentionPolicy = (
210
+ policy: DocumentChangeRetentionPolicy,
211
+ nowMs: number
212
+ ): void => {
213
+ if (
214
+ !Number.isSafeInteger(nowMs) ||
215
+ nowMs < 0 ||
216
+ !Number.isSafeInteger(policy.maxAgeDays) ||
217
+ policy.maxAgeDays < 1 ||
218
+ policy.maxAgeDays > 3650 ||
219
+ !Number.isSafeInteger(policy.maxEntries) ||
220
+ policy.maxEntries < 1 ||
221
+ policy.maxEntries > 1_000_000 ||
222
+ !Number.isSafeInteger(policy.maxBytes) ||
223
+ policy.maxBytes < 1 ||
224
+ policy.maxBytes > 1024 * 1024 * 1024
225
+ ) {
226
+ throw new RangeError("Invalid document change retention policy");
227
+ }
228
+ };
@@ -0,0 +1,395 @@
1
+ /**
2
+ * Shared read services for retained knowledge changes, structural diffs, and
3
+ * bounded inbound dependency impact.
4
+ */
5
+
6
+ import type {
7
+ DocumentChangeRow,
8
+ DocumentChangeStructureDelta,
9
+ DocumentRow,
10
+ StorePort,
11
+ } from "../store/types";
12
+
13
+ import {
14
+ decodeDocumentChangeCursor,
15
+ encodeDocumentChangeCursor,
16
+ } from "./change-journal";
17
+ import { resolveDocRef } from "./ref-parser";
18
+
19
+ const DEFAULT_CHANGE_LIMIT = 100;
20
+ const MAX_CHANGE_LIMIT = 1000;
21
+ const MAX_DIFF_SCAN_PAGES = 10;
22
+ const DIFF_SCAN_PAGE_SIZE = 1000;
23
+
24
+ export interface KnowledgeChangeSnapshot {
25
+ relPath: string;
26
+ docid: string;
27
+ uri: string;
28
+ sourceHash: string;
29
+ mirrorHash: string | null;
30
+ active: boolean;
31
+ }
32
+
33
+ export interface KnowledgeChange {
34
+ id: string;
35
+ kind: DocumentChangeRow["kind"];
36
+ collection: string;
37
+ observedAt: string;
38
+ previous: KnowledgeChangeSnapshot | null;
39
+ current: KnowledgeChangeSnapshot | null;
40
+ structureDelta: DocumentChangeStructureDelta;
41
+ }
42
+
43
+ export interface KnowledgeChangesResult {
44
+ schemaVersion: "1.0";
45
+ changes: KnowledgeChange[];
46
+ page: {
47
+ nextCursor: string | null;
48
+ earliestCursor: string;
49
+ latestCursor: string;
50
+ cursorExpired: boolean;
51
+ truncated: boolean;
52
+ retentionTruncated: boolean;
53
+ };
54
+ warnings: string[];
55
+ }
56
+
57
+ export interface ListKnowledgeChangesInput {
58
+ since?: string;
59
+ collection?: string;
60
+ limit?: number;
61
+ }
62
+
63
+ export interface KnowledgeDocument {
64
+ id: string;
65
+ uri: string;
66
+ title: string | null;
67
+ collection: string;
68
+ relPath: string;
69
+ active?: boolean;
70
+ }
71
+
72
+ export interface KnowledgeDiffResult {
73
+ schemaVersion: "1.0";
74
+ status: "available" | "expired" | "unavailable";
75
+ document: KnowledgeDocument & { active: boolean };
76
+ change: KnowledgeChange | null;
77
+ content: {
78
+ status: "not_retained";
79
+ reason: "journal_metadata_only";
80
+ };
81
+ history: {
82
+ status: "available" | "partial" | "unavailable";
83
+ reason:
84
+ | null
85
+ | "structure_delta_truncated"
86
+ | "change_expired"
87
+ | "no_retained_change"
88
+ | "change_not_found";
89
+ };
90
+ warnings: string[];
91
+ }
92
+
93
+ export type KnowledgeDeltaServiceResult<T> =
94
+ | { success: true; data: T }
95
+ | { success: false; error: string; isValidation?: boolean };
96
+
97
+ const snapshot = (
98
+ row: DocumentChangeRow,
99
+ side: "old" | "new"
100
+ ): KnowledgeChangeSnapshot | null => {
101
+ const prefix = side === "old" ? "old" : "new";
102
+ const relPath = row[`${prefix}RelPath`];
103
+ const docid = row[`${prefix}Docid`];
104
+ const uri = row[`${prefix}Uri`];
105
+ const sourceHash = row[`${prefix}SourceHash`];
106
+ const active = row[`${prefix}Active`];
107
+ if (
108
+ relPath === null ||
109
+ docid === null ||
110
+ uri === null ||
111
+ sourceHash === null ||
112
+ active === null
113
+ ) {
114
+ return null;
115
+ }
116
+ return {
117
+ relPath,
118
+ docid,
119
+ uri,
120
+ sourceHash,
121
+ mirrorHash: row[`${prefix}MirrorHash`],
122
+ active,
123
+ };
124
+ };
125
+
126
+ export const projectKnowledgeChange = (
127
+ row: DocumentChangeRow
128
+ ): KnowledgeChange => ({
129
+ id: encodeDocumentChangeCursor(row.sequence),
130
+ kind: row.kind,
131
+ collection: row.collection,
132
+ observedAt: new Date(row.observedAtMs).toISOString(),
133
+ previous: snapshot(row, "old"),
134
+ current: snapshot(row, "new"),
135
+ structureDelta: row.structureDelta,
136
+ });
137
+
138
+ const parseSince = (
139
+ since: string | undefined
140
+ ): { cursor?: string; observedAfterMs?: number } | { error: string } => {
141
+ if (since === undefined) return {};
142
+ const trimmed = since.trim();
143
+ if (!trimmed) return { error: "since cannot be empty" };
144
+ try {
145
+ decodeDocumentChangeCursor(trimmed);
146
+ return { cursor: trimmed };
147
+ } catch {
148
+ const observedAfterMs = Date.parse(trimmed);
149
+ return Number.isFinite(observedAfterMs)
150
+ ? { observedAfterMs }
151
+ : { error: "since must be an ISO-8601 time or opaque change cursor" };
152
+ }
153
+ };
154
+
155
+ export async function listKnowledgeChanges(
156
+ store: StorePort,
157
+ input: ListKnowledgeChangesInput = {}
158
+ ): Promise<KnowledgeDeltaServiceResult<KnowledgeChangesResult>> {
159
+ if ((input.since?.length ?? 0) > 512) {
160
+ return {
161
+ success: false,
162
+ error: "since must be at most 512 characters",
163
+ isValidation: true,
164
+ };
165
+ }
166
+ const collection = input.collection?.trim();
167
+ if (
168
+ input.collection !== undefined &&
169
+ (!collection || collection.length > 256)
170
+ ) {
171
+ return {
172
+ success: false,
173
+ error: "collection must be between 1 and 256 characters",
174
+ isValidation: true,
175
+ };
176
+ }
177
+ const limit = input.limit ?? DEFAULT_CHANGE_LIMIT;
178
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_CHANGE_LIMIT) {
179
+ return {
180
+ success: false,
181
+ error: `limit must be between 1 and ${MAX_CHANGE_LIMIT}`,
182
+ isValidation: true,
183
+ };
184
+ }
185
+ const since = parseSince(input.since);
186
+ if ("error" in since) {
187
+ return { success: false, error: since.error, isValidation: true };
188
+ }
189
+ const listed = await store.listDocumentChanges({
190
+ ...since,
191
+ collection,
192
+ limit,
193
+ });
194
+ if (!listed.ok) {
195
+ return {
196
+ success: false,
197
+ error: listed.error.message,
198
+ isValidation: listed.error.code === "INVALID_INPUT",
199
+ };
200
+ }
201
+ const retentionTruncated =
202
+ decodeDocumentChangeCursor(listed.value.earliestCursor) > 0;
203
+ const warnings: string[] = [];
204
+ if (listed.value.cursorExpired) {
205
+ warnings.push(
206
+ `Requested cursor expired; resume from ${listed.value.earliestCursor}`
207
+ );
208
+ } else if (retentionTruncated && !since.cursor) {
209
+ warnings.push("Earlier journal history was removed by retention");
210
+ }
211
+ return {
212
+ success: true,
213
+ data: {
214
+ schemaVersion: "1.0",
215
+ changes: listed.value.changes.map(projectKnowledgeChange),
216
+ page: {
217
+ nextCursor: listed.value.nextCursor,
218
+ earliestCursor: listed.value.earliestCursor,
219
+ latestCursor: listed.value.latestCursor,
220
+ cursorExpired: listed.value.cursorExpired,
221
+ truncated: listed.value.truncated,
222
+ retentionTruncated,
223
+ },
224
+ warnings,
225
+ },
226
+ };
227
+ }
228
+
229
+ const document = (
230
+ row: DocumentRow
231
+ ): KnowledgeDocument & { active: boolean } => ({
232
+ id: row.docid,
233
+ uri: row.uri,
234
+ title: row.title,
235
+ collection: row.collection,
236
+ relPath: row.relPath,
237
+ active: row.active,
238
+ });
239
+
240
+ async function latestRetainedChange(
241
+ store: StorePort,
242
+ documentId: number
243
+ ): Promise<
244
+ | { ok: true; row: DocumentChangeRow | null; retentionTruncated: boolean }
245
+ | { ok: false; error: string }
246
+ > {
247
+ let cursor: string | undefined;
248
+ let latest: DocumentChangeRow | null = null;
249
+ let retentionTruncated = false;
250
+ for (let pageIndex = 0; pageIndex < MAX_DIFF_SCAN_PAGES; pageIndex += 1) {
251
+ const page = await store.listDocumentChanges({
252
+ cursor,
253
+ documentId,
254
+ limit: DIFF_SCAN_PAGE_SIZE,
255
+ });
256
+ if (!page.ok) return { ok: false, error: page.error.message };
257
+ retentionTruncated =
258
+ decodeDocumentChangeCursor(page.value.earliestCursor) > 0;
259
+ latest = page.value.changes.at(-1) ?? latest;
260
+ if (!page.value.nextCursor) {
261
+ return { ok: true, row: latest, retentionTruncated };
262
+ }
263
+ cursor = page.value.nextCursor;
264
+ }
265
+ return { ok: false, error: "Retained change scan exceeded its bound" };
266
+ }
267
+
268
+ async function exactRetainedChange(
269
+ store: StorePort,
270
+ documentId: number,
271
+ changeId: string
272
+ ): Promise<
273
+ | { ok: true; row: DocumentChangeRow | null; expired: boolean }
274
+ | { ok: false; error: string; isValidation?: boolean }
275
+ > {
276
+ let sequence: number;
277
+ try {
278
+ sequence = decodeDocumentChangeCursor(changeId);
279
+ } catch {
280
+ return { ok: false, error: "Invalid change id", isValidation: true };
281
+ }
282
+ if (sequence < 1) {
283
+ return { ok: false, error: "Invalid change id", isValidation: true };
284
+ }
285
+ const page = await store.listDocumentChanges({
286
+ cursor: encodeDocumentChangeCursor(sequence - 1),
287
+ documentId,
288
+ limit: 1,
289
+ });
290
+ if (!page.ok) {
291
+ return {
292
+ ok: false,
293
+ error: page.error.message,
294
+ isValidation: page.error.code === "INVALID_INPUT",
295
+ };
296
+ }
297
+ const row = page.value.changes[0] ?? null;
298
+ return {
299
+ ok: true,
300
+ row: row?.sequence === sequence ? row : null,
301
+ expired: page.value.cursorExpired,
302
+ };
303
+ }
304
+
305
+ export async function getKnowledgeDiff(
306
+ store: StorePort,
307
+ ref: string,
308
+ changeId?: string
309
+ ): Promise<KnowledgeDeltaServiceResult<KnowledgeDiffResult>> {
310
+ const normalizedRef = ref.trim();
311
+ if (!normalizedRef || normalizedRef.length > 4096) {
312
+ return {
313
+ success: false,
314
+ error: "ref must be between 1 and 4096 characters",
315
+ isValidation: true,
316
+ };
317
+ }
318
+ const normalizedChangeId = changeId?.trim();
319
+ if (
320
+ changeId !== undefined &&
321
+ (!normalizedChangeId || normalizedChangeId.length > 512)
322
+ ) {
323
+ return {
324
+ success: false,
325
+ error: "changeId must be between 1 and 512 characters",
326
+ isValidation: true,
327
+ };
328
+ }
329
+ const resolved = await resolveDocRef(store, normalizedRef);
330
+ if ("error" in resolved) {
331
+ return {
332
+ success: false,
333
+ error: resolved.error,
334
+ isValidation: resolved.isValidation,
335
+ };
336
+ }
337
+ let row: DocumentChangeRow | null;
338
+ let status: KnowledgeDiffResult["status"];
339
+ let history: KnowledgeDiffResult["history"];
340
+ const warnings = ["Source bodies are not retained in the change journal"];
341
+ if (normalizedChangeId) {
342
+ const exact = await exactRetainedChange(
343
+ store,
344
+ resolved.doc.id,
345
+ normalizedChangeId
346
+ );
347
+ if (!exact.ok) return { success: false, ...exact };
348
+ row = exact.row;
349
+ status = exact.expired ? "expired" : row ? "available" : "unavailable";
350
+ history = exact.expired
351
+ ? { status: "unavailable", reason: "change_expired" }
352
+ : row
353
+ ? row.structureDelta.truncated
354
+ ? { status: "partial", reason: "structure_delta_truncated" }
355
+ : { status: "available", reason: null }
356
+ : { status: "unavailable", reason: "change_not_found" };
357
+ } else {
358
+ const latest = await latestRetainedChange(store, resolved.doc.id);
359
+ if (!latest.ok) {
360
+ return { success: false, error: latest.error };
361
+ }
362
+ row = latest.row;
363
+ status = row ? "available" : "unavailable";
364
+ history = row
365
+ ? row.structureDelta.truncated
366
+ ? { status: "partial", reason: "structure_delta_truncated" }
367
+ : { status: "available", reason: null }
368
+ : { status: "unavailable", reason: "no_retained_change" };
369
+ if (latest.retentionTruncated) {
370
+ warnings.push("Earlier journal history was removed by retention");
371
+ }
372
+ }
373
+ return {
374
+ success: true,
375
+ data: {
376
+ schemaVersion: "1.0",
377
+ status,
378
+ document: document(resolved.doc),
379
+ change: row ? projectKnowledgeChange(row) : null,
380
+ content: {
381
+ status: "not_retained",
382
+ reason: "journal_metadata_only",
383
+ },
384
+ history,
385
+ warnings,
386
+ },
387
+ };
388
+ }
389
+
390
+ export { analyzeKnowledgeImpact } from "./knowledge-impact";
391
+ export type {
392
+ KnowledgeImpactEvidenceStep,
393
+ KnowledgeImpactInput,
394
+ KnowledgeImpactResult,
395
+ } from "./knowledge-impact";