@prodero/curator 0.0.0-stage → 0.1.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 (137) hide show
  1. package/ARCHITECTURE.md +298 -0
  2. package/CHANGELOG.md +16 -0
  3. package/CONTRIBUTING.md +9 -0
  4. package/LICENSE +21 -0
  5. package/README.md +94 -2
  6. package/SECURITY.md +7 -0
  7. package/bin/prodero +7 -0
  8. package/dist/cli.js +228 -0
  9. package/dist/context-instructions.js +2 -0
  10. package/dist/contracts.js +50 -0
  11. package/dist/conversation-presentation.js +262 -0
  12. package/dist/core/agent-instructions.js +39 -0
  13. package/dist/core/applicability-label.js +11 -0
  14. package/dist/core/applicability.js +143 -0
  15. package/dist/core/bootstrap.js +129 -0
  16. package/dist/core/consolidation.js +315 -0
  17. package/dist/core/constants.js +21 -0
  18. package/dist/core/context-allocation.js +54 -0
  19. package/dist/core/context-snapshot.js +12 -0
  20. package/dist/core/context.js +489 -0
  21. package/dist/core/curation-clauses.js +68 -0
  22. package/dist/core/curation-evidence.js +98 -0
  23. package/dist/core/curation-graphical.js +105 -0
  24. package/dist/core/curation-review-presentation.js +4 -0
  25. package/dist/core/curation-review-state.js +62 -0
  26. package/dist/core/curation-review.js +573 -0
  27. package/dist/core/curation-revision.js +106 -0
  28. package/dist/core/curation-semantic-contract.js +239 -0
  29. package/dist/core/curation-semantic-jobs.js +280 -0
  30. package/dist/core/diagnostics.js +17 -0
  31. package/dist/core/format.js +114 -0
  32. package/dist/core/ids.js +6 -0
  33. package/dist/core/imports.js +169 -0
  34. package/dist/core/index.js +93 -0
  35. package/dist/core/instruction-review.js +86 -0
  36. package/dist/core/integrity-files.js +174 -0
  37. package/dist/core/intelligence.js +95 -0
  38. package/dist/core/knowledge-analysis.js +46 -0
  39. package/dist/core/knowledge-authority.js +143 -0
  40. package/dist/core/knowledge-curation.js +316 -0
  41. package/dist/core/knowledge-engine.js +475 -0
  42. package/dist/core/knowledge-graph.js +191 -0
  43. package/dist/core/knowledge-lifecycle.js +212 -0
  44. package/dist/core/knowledge-management.js +113 -0
  45. package/dist/core/knowledge-migration.js +84 -0
  46. package/dist/core/knowledge-policy.js +38 -0
  47. package/dist/core/knowledge-provenance.js +90 -0
  48. package/dist/core/knowledge-write-policy.js +85 -0
  49. package/dist/core/language.js +14 -0
  50. package/dist/core/matching.js +6 -0
  51. package/dist/core/memory-index.js +239 -0
  52. package/dist/core/memory-intelligence.js +288 -0
  53. package/dist/core/memory-link-repair.js +66 -0
  54. package/dist/core/memory-operations.js +122 -0
  55. package/dist/core/memory-pressure.js +212 -0
  56. package/dist/core/memory-sync-curator.js +62 -0
  57. package/dist/core/memory-sync.js +463 -0
  58. package/dist/core/memory-transformation-bundle.js +163 -0
  59. package/dist/core/memory-transformation-proposal.js +135 -0
  60. package/dist/core/memory-transformation.js +108 -0
  61. package/dist/core/memory.js +186 -0
  62. package/dist/core/migrations.js +569 -0
  63. package/dist/core/path-scope.js +27 -0
  64. package/dist/core/plugin-files.js +116 -0
  65. package/dist/core/project-root.js +39 -0
  66. package/dist/core/project.js +634 -0
  67. package/dist/core/promotion.js +279 -0
  68. package/dist/core/reconciliation.js +61 -0
  69. package/dist/core/secrets.js +67 -0
  70. package/dist/core/semantic-safety.js +33 -0
  71. package/dist/core/session-index.js +112 -0
  72. package/dist/core/session-review.js +150 -0
  73. package/dist/core/sessions.js +289 -0
  74. package/dist/core/settings.js +76 -0
  75. package/dist/core/skill-curation.js +239 -0
  76. package/dist/core/skill-polish.js +41 -0
  77. package/dist/core/skills.js +368 -0
  78. package/dist/core/storage.js +408 -0
  79. package/dist/core/sync-transaction.js +254 -0
  80. package/dist/core/telemetry.js +64 -0
  81. package/dist/core/templates.js +53 -0
  82. package/dist/core/types.js +2 -0
  83. package/dist/core/update.js +320 -0
  84. package/dist/curation-review-adapter.js +92 -0
  85. package/dist/diagnostic-boundary.js +54 -0
  86. package/dist/filesystem/permissions.js +39 -0
  87. package/dist/hosts/adapters.js +119 -0
  88. package/dist/hosts/files.js +133 -0
  89. package/dist/hosts/manager.js +150 -0
  90. package/dist/hosts/public-entry.js +47 -0
  91. package/dist/hosts/runtime-command.js +66 -0
  92. package/dist/hosts/runtime.js +139 -0
  93. package/dist/hosts/wizard.js +114 -0
  94. package/dist/knowledge-table.js +38 -0
  95. package/dist/mcp-development.js +69 -0
  96. package/dist/mcp-workspace.js +141 -0
  97. package/dist/memory-presentation.js +172 -0
  98. package/dist/plugins/api.js +2 -0
  99. package/dist/plugins/context.js +20 -0
  100. package/dist/plugins/mcp.js +72 -0
  101. package/dist/plugins/registry.js +106 -0
  102. package/dist/presentation-cards.js +32 -0
  103. package/dist/presentation-server.js +79 -0
  104. package/dist/presentation-trace.js +174 -0
  105. package/dist/presentation.js +412 -0
  106. package/dist/providers/mcp-sampling.js +45 -0
  107. package/dist/server.js +656 -0
  108. package/dist/settings-definition.js +22 -0
  109. package/dist/skill-instructions.js +2 -0
  110. package/dist-ui/index.html +214 -0
  111. package/docs/APPLICABILITY.md +151 -0
  112. package/docs/COMPATIBILITY.md +49 -0
  113. package/docs/CONTEXT-SELECTION-V2.md +61 -0
  114. package/docs/CURATION-APP-REVIEW.md +156 -0
  115. package/docs/CURATION-CLAUSE-REFINEMENT.md +45 -0
  116. package/docs/IMPORTER-CURATION.md +142 -0
  117. package/docs/KNOWLEDGE-AUTHORITY.md +165 -0
  118. package/docs/KNOWLEDGE-SCHEMA-V2.md +150 -0
  119. package/docs/MIGRATIONS.md +99 -0
  120. package/docs/PRIVACY.md +28 -0
  121. package/docs/RECOVERY.md +56 -0
  122. package/docs/RELEASING.md +40 -0
  123. package/docs/hosts.md +134 -0
  124. package/docs/plugins.md +83 -0
  125. package/docs/ui-themes.md +43 -0
  126. package/instructions/AGENTS.md +33 -0
  127. package/package.json +88 -4
  128. package/plugins/migrate_ai_studio/README.md +101 -0
  129. package/plugins/migrate_ai_studio/index.cjs +396 -0
  130. package/plugins/migrate_ai_studio/instructions/audit.md +25 -0
  131. package/plugins/migrate_ai_studio/instructions/classify.md +41 -0
  132. package/plugins/migrate_ai_studio/instructions/migrate.md +34 -0
  133. package/plugins/migrate_ai_studio/instructions/verify.md +20 -0
  134. package/plugins/migrate_ai_studio/plugin.json +11 -0
  135. package/plugins/migrate_ai_studio/schema.cjs +20 -0
  136. package/plugins/migrate_ai_studio/ui/dist/index.html +77 -0
  137. package/prodero.release.json +5 -0
@@ -0,0 +1,279 @@
1
+ import { applicabilitySchema, sameApplicability } from "./applicability.js";
2
+ import { CONTEXT_POLICIES, KNOWLEDGE_AUTHORITIES } from "./knowledge-policy.js";
3
+ import { reconcileCandidate } from './reconciliation.js';
4
+ import { assertNoSecrets } from "./secrets.js";
5
+ import { z } from "zod";
6
+ import { buildIndex } from "./index.js";
7
+ import { intelligenceTelemetry } from "./intelligence.js";
8
+ import { sortableId } from "./ids.js";
9
+ import { loadMemoryCatalog } from "./memory-index.js";
10
+ import { tokens } from "./matching.js";
11
+ import { renderKnowledge } from "./memory.js";
12
+ import { exactKnowledge, isExtractivelyGrounded } from "./semantic-safety.js";
13
+ import { listCheckpoints, sha256 } from "./storage.js";
14
+ import { commitSyncTransaction, fileHash, recoverSyncTransaction, safeProjectPath, withSyncLock, ownsSyncLock } from "./sync-transaction.js";
15
+ const dispositionSchema = z.enum(["new", "merge", "supersede", "covered", "conflict"]);
16
+ const revisionSchema = z.object({ id: z.string(), layer: z.enum(["local", "versioned"]), path: z.string(), hash: z.string() }).strict();
17
+ const itemSchema = z.object({
18
+ applicability: applicabilitySchema.optional(), version: z.union([z.literal(1), z.literal(2), z.literal(3)]).optional(), contextPolicy: z.enum(CONTEXT_POLICIES).optional(), authority: z.enum(KNOWLEDGE_AUTHORITIES).optional(),
19
+ kind: z.literal("instruction").optional(), enforcement: z.enum(CONTEXT_POLICIES).optional(),
20
+ archivedAt: z.string().optional(),
21
+ id: z.string(), scope: z.enum(["global", "module", "task"]), scopeRef: z.string().nullable(), scopeKind: z.enum(["path", "topic"]).optional(),
22
+ category: z.enum(["principle", "invariant", "decision", "constraint", "lesson", "state", "instruction"]),
23
+ priority: z.enum(["critical", "important", "normal", "contextual"]), status: z.enum(["active", "superseded", "deprecated", "consolidated", "duplicate", "disputed"]),
24
+ title: z.string(), content: z.string(), createdAt: z.string(), updatedAt: z.string(), sourceSessions: z.array(z.string()),
25
+ derivedFrom: z.array(z.string()), supersedes: z.array(z.string()), related: z.array(z.string()),
26
+ relations: z.array(z.object({ type: z.enum(["related_to", "depends_on", "implements", "applies_to", "supersedes", "superseded_by", "consolidates", "consolidated_into", "contradicts", "learned_from"]), target: z.string() }).strict()).optional(),
27
+ evidence: z.array(z.object({ kind: z.enum(["session_observation", "source_file", "test", "git_commit", "documentation", "user_instruction"]), ref: z.string() }).strict()).optional(),
28
+ }).strict();
29
+ const promotionActionSchema = z.object({
30
+ disposition: dispositionSchema, localIds: z.array(z.string()).min(1), targetId: z.string().nullable(), before: itemSchema.nullable(), draft: itemSchema.nullable(),
31
+ reason: z.string(), recommended: z.boolean(), recommendationSignals: z.array(z.string()),
32
+ }).strict();
33
+ export const promotionPreviewSchema = z.object({
34
+ version: z.literal(1), previewId: z.string(), createdAt: z.string(), mode: z.enum(["deterministic", "assisted"]), notice: z.string().optional(),
35
+ actions: z.array(promotionActionSchema), revisions: z.array(revisionSchema),
36
+ summary: z.object({ new: z.number(), merge: z.number(), supersede: z.number(), covered: z.number(), conflict: z.number() }).strict(),
37
+ }).strict();
38
+ const withPromotionLock = (root, operation, options) => ownsSyncLock(root) ? operation() : withSyncLock(root, operation, options);
39
+ const unique = (values) => [...new Set(values)].sort();
40
+ const current = (item) => item.status === "active" || item.status === "disputed";
41
+ const samePolicy = (a, b) => a.contextPolicy === b.contextPolicy && a.authority === b.authority;
42
+ const sameBoundary = (a, b) => a.scope === b.scope && a.scopeRef === b.scopeRef && a.scopeKind === b.scopeKind && a.category === b.category && sameApplicability(a, b);
43
+ function similarity(a, b) {
44
+ const left = new Set(a.tokens), right = new Set(b.tokens);
45
+ const intersection = [...left].filter((token) => right.has(token)).length;
46
+ return intersection / Math.max(1, Math.min(left.size, right.size));
47
+ }
48
+ export function promotionSignals(local, versioned, now = new Date()) {
49
+ let score = 0;
50
+ const signals = [];
51
+ if (local.sourceSessionCount >= 2) {
52
+ score += 2;
53
+ signals.push(`Recurred across ${local.sourceSessionCount} sessions`);
54
+ }
55
+ if (local.priority === "critical") {
56
+ score += 3;
57
+ signals.push("Critical knowledge");
58
+ }
59
+ else if (local.priority === "important") {
60
+ score += 2;
61
+ signals.push("Important knowledge");
62
+ }
63
+ if (["principle", "invariant", "constraint", "instruction", "decision"].includes(local.category)) {
64
+ score += 2;
65
+ signals.push(`Architecturally significant ${local.category}`);
66
+ }
67
+ const ageDays = (now.getTime() - Date.parse(local.updatedAt)) / 86_400_000;
68
+ if (Number.isFinite(ageDays) && ageDays >= 7) {
69
+ score += 1;
70
+ signals.push("Persisted locally for at least seven days");
71
+ }
72
+ if (local.relations.length || local.related.length) {
73
+ score += 1;
74
+ signals.push("Connected to other knowledge");
75
+ }
76
+ const covered = versioned.some((item) => sameBoundary(local, item) && samePolicy(local, item) && item.normalizedContent === local.normalizedContent);
77
+ if (!covered) {
78
+ score += 1;
79
+ signals.push("Not already covered by Versioned Knowledge");
80
+ }
81
+ return { score, signals, recommended: score >= 4 && !covered };
82
+ }
83
+ export async function promotionRecommendations(root, options = {}) {
84
+ const catalog = await loadMemoryCatalog(root, options);
85
+ const versioned = catalog.summaries.filter((item) => item.layer === "versioned" && current(item));
86
+ return catalog.summaries.filter((item) => item.layer === "local" && current(item)).map((item) => ({ id: item.id, title: item.title, ...promotionSignals(item, versioned) }))
87
+ .sort((a, b) => Number(b.recommended) - Number(a.recommended) || b.score - a.score || a.id.localeCompare(b.id));
88
+ }
89
+ function draftFromLocal(local, now, target) {
90
+ return target ? {
91
+ ...target, title: local.title, content: local.content, updatedAt: now,
92
+ sourceSessions: unique([...target.sourceSessions, ...local.sourceSessions]), derivedFrom: unique([...target.derivedFrom, ...local.derivedFrom]),
93
+ related: unique([...target.related, ...local.related, local.id]), relations: [...(target.relations ?? []), ...(local.relations ?? [])].filter((relation, index, all) => all.findIndex((other) => other.type === relation.type && other.target === relation.target) === index),
94
+ evidence: [...(target.evidence ?? []), ...(local.evidence ?? [])],
95
+ } : { ...local, id: sortableId("mem"), status: "active", createdAt: now, updatedAt: now, related: unique(local.related) };
96
+ }
97
+ function deterministicAction(local, localSummary, candidates, recommendation, now) {
98
+ const exact = candidates.find(({ item }) => sameBoundary(local, item) && samePolicy(local, item) && exactKnowledge(item.content) === exactKnowledge(local.content));
99
+ if (exact)
100
+ return { disposition: "covered", localIds: [local.id], targetId: exact.item.id, before: exact.item, draft: null, reason: "Versioned Knowledge already covers the local knowledge exactly.", recommended: recommendation.recommended, recommendationSignals: recommendation.signals };
101
+ if (local.status === "disputed")
102
+ return { disposition: "conflict", localIds: [local.id], targetId: null, before: null, draft: null, reason: "Disputed Local Knowledge requires focused human resolution before publication.", recommended: recommendation.recommended, recommendationSignals: recommendation.signals };
103
+ const explicit = candidates.find(({ item }) => local.supersedes.includes(item.id));
104
+ if (explicit) {
105
+ const draft = draftFromLocal(local, now);
106
+ draft.supersedes = unique([...draft.supersedes, explicit.item.id]);
107
+ return { disposition: "supersede", localIds: [local.id], targetId: explicit.item.id, before: explicit.item, draft, reason: "The local knowledge explicitly supersedes current Versioned Knowledge.", recommended: recommendation.recommended, recommendationSignals: recommendation.signals };
108
+ }
109
+ const related = candidates.filter(({ item }) => sameBoundary(local, item) && samePolicy(local, item)).sort((a, b) => similarity(localSummary, b.summary) - similarity(localSummary, a.summary) || a.item.id.localeCompare(b.item.id))[0];
110
+ if (related) {
111
+ const score = similarity(localSummary, related.summary);
112
+ if (exactKnowledge(related.item.content).includes(exactKnowledge(local.content)))
113
+ return { disposition: "covered", localIds: [local.id], targetId: related.item.id, before: related.item, draft: null, reason: "Versioned Knowledge already contains the local observation.", recommended: recommendation.recommended, recommendationSignals: recommendation.signals };
114
+ if (score >= 0.65 && exactKnowledge(local.content).includes(exactKnowledge(related.item.content)))
115
+ return { disposition: "merge", localIds: [local.id], targetId: related.item.id, before: related.item, draft: draftFromLocal(local, now, related.item), reason: "The local observation extends an existing Versioned Knowledge item.", recommended: recommendation.recommended, recommendationSignals: recommendation.signals };
116
+ }
117
+ return { disposition: "new", localIds: [local.id], targetId: null, before: null, draft: draftFromLocal(local, now), reason: "No sufficiently similar Versioned Knowledge item covers this durable local knowledge.", recommended: recommendation.recommended, recommendationSignals: recommendation.signals };
118
+ }
119
+ async function assistedAction(provider, local, deterministic, candidates, now) {
120
+ const localTokens = tokens(`${local.title} ${local.content} ${local.scopeRef ?? ""}`);
121
+ const related = candidates.filter(({ item }) => sameBoundary(local, item) && samePolicy(local, item)).sort((a, b) => b.summary.tokens.filter((token) => localTokens.has(token)).length - a.summary.tokens.filter((token) => localTokens.has(token)).length || a.item.id.localeCompare(b.item.id)).slice(0, 12).map(({ item }) => item);
122
+ const candidate = { id: `candidate_${local.id}`, kind: "knowledge", title: local.title, content: local.content, category: local.category, scope: local.scope, scopeRef: local.scopeRef, scopeKind: local.scopeKind, priority: local.priority, applicability: local.applicability, sources: [local.id], evidence: [{ source: local.id, quote: local.content }], supersedes: local.supersedes };
123
+ const telemetry = intelligenceTelemetry(provider);
124
+ const response = await reconcileCandidate(candidate, related, provider, telemetry);
125
+ const targetId = response.sources.find((id) => id !== candidate.id) ?? null;
126
+ const target = targetId ? related.find((item) => item.id === targetId) : undefined;
127
+ const base = { localIds: [local.id], recommended: deterministic.recommended, recommendationSignals: deterministic.recommendationSignals, reason: response.reason };
128
+ if (response.action === "CREATE" && !targetId)
129
+ return { ...base, disposition: "new", targetId: null, before: null, draft: draftFromLocal(local, now) };
130
+ if (response.action === "NO_CHANGE" && target)
131
+ return { ...base, disposition: "covered", targetId, before: target, draft: null };
132
+ if (response.action === "MERGE" && target && response.canonical && sameBoundary(local, target) && sameBoundary({ ...response.canonical, applicability: response.canonical.applicability ?? target.applicability }, target) && isExtractivelyGrounded(response.canonical.content, [local.content, target.content])) {
133
+ return { ...base, disposition: "merge", targetId, before: target, draft: { ...draftFromLocal(local, now, target), ...response.canonical } };
134
+ }
135
+ if (response.action === "SUPERSEDE" && target && local.supersedes.includes(target.id)) {
136
+ const draft = draftFromLocal(local, now);
137
+ draft.supersedes = unique([...draft.supersedes, target.id]);
138
+ return { ...base, disposition: "supersede", targetId, before: target, draft };
139
+ }
140
+ if ((response.action === "CONFLICT" || response.action === "UNCERTAIN") && target)
141
+ return { ...base, disposition: "conflict", targetId, before: target, draft: null };
142
+ if (response.action === "LINK" && target) {
143
+ const draft = draftFromLocal(local, now);
144
+ draft.relations = [...(draft.relations ?? []), { type: "related_to", target: target.id }];
145
+ return { ...base, disposition: "new", targetId: null, before: null, draft };
146
+ }
147
+ return deterministic;
148
+ }
149
+ export async function previewKnowledgePromotion(root, options = {}) {
150
+ const catalog = await loadMemoryCatalog(root);
151
+ const localSummaries = catalog.summaries.filter((item) => item.layer === "local" && current(item));
152
+ const versionedSummaries = catalog.summaries.filter((item) => item.layer === "versioned" && current(item));
153
+ const recommendations = new Map(localSummaries.map((item) => [item.id, promotionSignals(item, versionedSummaries)]));
154
+ const selected = localSummaries.filter((item) => options.ids?.length ? options.ids.includes(item.id) : options.includeAll === false ? recommendations.get(item.id).recommended : true);
155
+ if (options.ids?.some((id) => !selected.some((item) => item.id === id)))
156
+ throw new Error("Promotion requires current Local Knowledge ids.");
157
+ const versioned = await Promise.all(versionedSummaries.map(async (summary) => ({ summary, item: await catalog.load(summary.id) })));
158
+ const now = new Date().toISOString();
159
+ const actions = [];
160
+ let notice;
161
+ for (const summary of selected.sort((a, b) => a.id.localeCompare(b.id))) {
162
+ const local = await catalog.load(summary.id);
163
+ const fallback = deterministicAction(local, summary, versioned, recommendations.get(summary.id), now);
164
+ if (options.provider && local.status !== "disputed") {
165
+ try {
166
+ actions.push(await assistedAction(options.provider, local, fallback, versioned, now));
167
+ }
168
+ catch {
169
+ actions.push(fallback);
170
+ notice = "Semantic reasoning was unavailable for at least one candidate; conservative deterministic proposals are shown.";
171
+ }
172
+ }
173
+ else
174
+ actions.push(fallback);
175
+ }
176
+ const targetWriters = new Map();
177
+ for (const action of actions.filter((candidate) => candidate.targetId && (candidate.disposition === "merge" || candidate.disposition === "supersede"))) {
178
+ const group = targetWriters.get(action.targetId) ?? [];
179
+ group.push(action);
180
+ targetWriters.set(action.targetId, group);
181
+ }
182
+ for (const group of targetWriters.values()) {
183
+ if (group.length < 2)
184
+ continue;
185
+ const superseding = group.filter((action) => action.disposition === "supersede");
186
+ const keep = superseding.length === 1 ? superseding[0] : null;
187
+ for (const action of group)
188
+ if (action !== keep)
189
+ Object.assign(action, { disposition: "conflict", draft: null, reason: "Multiple local proposals would modify the same Versioned Knowledge item; consolidate this conflict in a fresh focused review." });
190
+ }
191
+ const promotedIds = new Map();
192
+ for (const action of actions)
193
+ if (action.disposition !== "conflict") {
194
+ const publishedId = action.disposition === "covered" || action.disposition === "merge" ? action.targetId : action.draft?.id;
195
+ if (publishedId)
196
+ for (const id of action.localIds)
197
+ promotedIds.set(id, publishedId);
198
+ }
199
+ for (const action of actions)
200
+ if (action.draft) {
201
+ action.draft.related = unique(action.draft.related.map((id) => promotedIds.get(id) ?? id));
202
+ action.draft.supersedes = unique(action.draft.supersedes.map((id) => promotedIds.get(id) ?? id));
203
+ action.draft.relations = (action.draft.relations ?? []).map((relation) => ({ ...relation, target: promotedIds.get(relation.target) ?? relation.target }));
204
+ }
205
+ const involved = new Set(actions.flatMap((action) => [...action.localIds, ...(action.targetId ? [action.targetId] : [])]));
206
+ const revisions = await Promise.all(catalog.summaries.filter((item) => involved.has(item.id)).map(async (item) => ({ id: item.id, layer: item.layer, path: item.relativePath, hash: (await fileHash(await safeProjectPath(root, item.relativePath))) })));
207
+ const summary = { new: 0, merge: 0, supersede: 0, covered: 0, conflict: 0 };
208
+ for (const action of actions)
209
+ summary[action.disposition]++;
210
+ const base = promotionPreviewSchema.omit({ previewId: true }).parse({ version: 1, createdAt: now, mode: options.provider ? "assisted" : "deterministic", ...(notice ? { notice } : {}), actions, revisions: revisions.sort((a, b) => a.path.localeCompare(b.path)), summary });
211
+ return { ...base, previewId: sha256(JSON.stringify(base)) };
212
+ }
213
+ export async function applyKnowledgePromotion(root, rawPreview, approved) {
214
+ assertNoSecrets(rawPreview);
215
+ if (!approved)
216
+ throw new Error("Versioned Knowledge promotion requires workflow approval of the exact preview.");
217
+ const preview = promotionPreviewSchema.parse(rawPreview);
218
+ const { previewId, ...base } = preview;
219
+ if (sha256(JSON.stringify(base)) !== previewId)
220
+ throw new Error("Promotion preview was modified; generate a fresh review.");
221
+ return withPromotionLock(root, async () => {
222
+ await recoverSyncTransaction(root);
223
+ const previous = (await listCheckpoints(root)).find(c => c.note === `Approved Local to Versioned Knowledge promotion ${previewId}`);
224
+ if (previous)
225
+ return { previewId, checkpoint: previous.id, created: previous.createdMemory, updated: previous.updatedMemory, superseded: previous.supersededMemory, covered: previous.duplicateMemory, conflicts: previous.conflicts, published: previous.createdMemory.length + previous.updatedMemory.length, alreadyApplied: true, revisions: previous.promotionRevisions ?? [] };
226
+ for (const revision of preview.revisions)
227
+ if (await fileHash(await safeProjectPath(root, revision.path)) !== revision.hash)
228
+ throw new Error("Knowledge changed after promotion review; generate a fresh preview.");
229
+ const catalog = await loadMemoryCatalog(root);
230
+ const writes = [];
231
+ const created = [], updated = [], superseded = [], covered = [], conflicts = [];
232
+ const now = new Date().toISOString();
233
+ for (const action of preview.actions) {
234
+ const local = await catalog.load(action.localIds[0]);
235
+ const localSummary = catalog.summaries.find((item) => item.id === local.id && item.layer === "local");
236
+ if (!localSummary)
237
+ throw new Error("Promotion candidate is no longer Local Knowledge.");
238
+ if (action.disposition === "conflict") {
239
+ conflicts.push(local.id);
240
+ continue;
241
+ }
242
+ const localUpdated = { ...local, status: action.disposition === "covered" ? "duplicate" : "consolidated", updatedAt: now, related: unique([...local.related, ...(action.targetId ? [action.targetId] : []), ...(action.draft ? [action.draft.id] : [])]) };
243
+ writes.push({ path: localSummary.relativePath, before: (await fileHash(await safeProjectPath(root, localSummary.relativePath))), after: renderKnowledge(localUpdated) });
244
+ if (action.disposition === "covered") {
245
+ covered.push(local.id);
246
+ continue;
247
+ }
248
+ if (!action.draft)
249
+ throw new Error("Publishable promotion action is missing a reviewed draft.");
250
+ if (action.disposition === "merge") {
251
+ const target = catalog.summaries.find((item) => item.id === action.targetId && item.layer === "versioned");
252
+ if (!target || action.draft.id !== target.id)
253
+ throw new Error("Merge target is not current Versioned Knowledge.");
254
+ writes.push({ path: target.relativePath, before: (await fileHash(await safeProjectPath(root, target.relativePath))), after: renderKnowledge(action.draft) });
255
+ updated.push(target.id);
256
+ }
257
+ else {
258
+ writes.push({ path: `memory/${action.draft.id}.md`, before: null, after: renderKnowledge(action.draft) });
259
+ created.push(action.draft.id);
260
+ if (action.disposition === "supersede") {
261
+ const targetSummary = catalog.summaries.find((item) => item.id === action.targetId && item.layer === "versioned");
262
+ if (!targetSummary)
263
+ throw new Error("Supersession target is not current Versioned Knowledge.");
264
+ const target = await catalog.load(targetSummary.id);
265
+ const retired = { ...target, status: "superseded", updatedAt: now, related: unique([...target.related, action.draft.id]) };
266
+ writes.push({ path: targetSummary.relativePath, before: (await fileHash(await safeProjectPath(root, targetSummary.relativePath))), after: renderKnowledge(retired) });
267
+ superseded.push(target.id);
268
+ }
269
+ }
270
+ }
271
+ const checkpointId = sortableId("chk");
272
+ const checkpoints = await listCheckpoints(root);
273
+ const checkpoint = { id: checkpointId, createdAt: now, previousCheckpoint: checkpoints.at(-1)?.id ?? null, sessions: [], consolidation: "not-performed", createdMemory: created, createdSkills: [], updatedMemory: updated, supersededMemory: superseded, duplicateMemory: covered, conflicts, summary: { sessions: 0, candidates: preview.actions.length, created: created.length, consolidated: updated.length, duplicates: covered.length, superseded: superseded.length, conflicts: conflicts.length }, promotionRevisions: writes.map(w => ({ path: w.path, hash: sha256(w.after) })), note: `Approved Local to Versioned Knowledge promotion ${preview.previewId}` };
274
+ await commitSyncTransaction(root, [...writes, { path: `checkpoints/${checkpointId}.json`, before: null, after: `${JSON.stringify(checkpoint, null, 2)}\n` }], { policyMode: "exact" });
275
+ await buildIndex(root);
276
+ return { previewId, checkpoint: checkpointId, created, updated, superseded, covered, conflicts, published: created.length + updated.length, alreadyApplied: false, revisions: checkpoint.promotionRevisions };
277
+ });
278
+ }
279
+ //# sourceMappingURL=promotion.js.map
@@ -0,0 +1,61 @@
1
+ import { exactKnowledge, evidenceSentences } from './semantic-safety.js';
2
+ import { intelligenceTelemetry, INTELLIGENCE_LIMITS, reconcileSchema, requestIntelligence } from './intelligence.js';
3
+ import { estimateTokens } from './telemetry.js';
4
+ /** Offline classification uses proof, not polarity, titles, dates or token overlap. */
5
+ export function deterministicClassification(candidate, old) {
6
+ if (candidate.supersedes.includes(old.id))
7
+ return 'state_transition';
8
+ if (exactKnowledge(candidate.content) === exactKnowledge(old.content))
9
+ return 'duplicate';
10
+ const incoming = evidenceSentences(candidate.content), previous = evidenceSentences(old.content);
11
+ if (incoming.every(s => previous.includes(s)))
12
+ return 'supporting';
13
+ if (previous.every(s => incoming.includes(s)))
14
+ return 'refinement';
15
+ return 'unresolved';
16
+ }
17
+ /** Legacy actions remain readable; uncertainty alone never establishes incompatibility. */
18
+ export function actionClassification(action) {
19
+ return action.classification ?? { SUPERSEDE: 'state_transition', MERGE: 'refinement', NO_CHANGE: 'supporting', CONFLICT: 'potential_conflict', UNCERTAIN: 'unresolved', LINK: 'independent', CREATE: 'independent' }[action.action];
20
+ }
21
+ export function normalizeReconciliation(action, hasCandidate = true) {
22
+ const classification = actionClassification(action);
23
+ if (classification === 'potential_conflict' && action.sources.length < 2)
24
+ throw new Error('Potential conflict requires incompatible evidence.');
25
+ if (action.action === 'SUPERSEDE' && classification !== 'state_transition')
26
+ throw new Error('Supersession requires state_transition classification.');
27
+ if (action.action === 'NO_CHANGE' && !['duplicate', 'supporting'].includes(classification))
28
+ throw new Error('No change requires duplicate or supporting evidence.');
29
+ if (classification === 'potential_conflict' && !['CONFLICT', 'UNCERTAIN'].includes(action.action))
30
+ throw new Error('Potential conflict requires a conflict action.');
31
+ if (['CONFLICT', 'UNCERTAIN'].includes(action.action) && classification !== 'potential_conflict') {
32
+ return { ...action, classification, action: !hasCandidate || action.sources.length > 1 ? 'LINK' : 'CREATE', canonical: null };
33
+ }
34
+ return { ...action, classification };
35
+ }
36
+ /** Both session review and consolidation use this bounded semantic reconciliation path. */
37
+ export async function reconcileCandidate(candidate, related, provider, telemetry = intelligenceTelemetry(provider)) {
38
+ const bounded = [];
39
+ for (const old of related.slice(0, INTELLIGENCE_LIMITS.relatedMemories)) {
40
+ if (estimateTokens(JSON.stringify({ candidate, related: [...bounded, old] })) <= INTELLIGENCE_LIMITS.reconcileInputTokens)
41
+ bounded.push(old);
42
+ }
43
+ const action = reconcileSchema.parse(await requestIntelligence(provider, 'reconcile', { candidate, related: bounded }, telemetry)).actions[0];
44
+ const allowed = new Set([candidate.id, ...bounded.map(m => m.id)]);
45
+ if (!action.sources.includes(candidate.id) || new Set(action.sources).size !== action.sources.length || action.sources.some(id => !allowed.has(id)))
46
+ throw new Error('Invalid reconciliation sources: unknown, duplicate or out-of-area ID.');
47
+ return normalizeReconciliation(action);
48
+ }
49
+ export function reconciliationSignal(kind, memory) {
50
+ const detail = {
51
+ state_transition: 'Knowledge evolution detected — newer evidence updates earlier project state.',
52
+ refinement: 'Knowledge refined — newer evidence adds detail to earlier knowledge.',
53
+ duplicate: 'Already covered by existing knowledge.',
54
+ supporting: 'Supporting evidence confirms earlier knowledge.',
55
+ potential_conflict: 'Potential conflict — incompatible claims need review to determine the current state.',
56
+ unresolved: 'Related evidence will be reconciled during consolidation.',
57
+ independent: 'Related knowledge provides additional context.',
58
+ }[kind];
59
+ return { kind, detail, severity: kind === 'potential_conflict' ? 'warning' : 'info', requiresReview: kind === 'potential_conflict', ...(memory ? { memory: { id: memory.id, title: memory.title, content: memory.content } } : {}) };
60
+ }
61
+ //# sourceMappingURL=reconciliation.js.map
@@ -0,0 +1,67 @@
1
+ /** Best-effort screening, shared by core storage and plugin/provider boundaries. Not complete DLP. */
2
+ function placeholder(value) {
3
+ return /^(?:<[^>]+>|\$[\w{][\w}]*|\$\{[^}]+\}|process\.env\.[\w]+|os\.environ(?:\[[^\]]+\]|\.get\([^)]*\))|example|placeholder|changeme|your[_-].*|redacted|\*+|x{3,}|undefined|null)$/i.test(value);
4
+ }
5
+ export function hasSecret(value) {
6
+ if (/-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|npm_[A-Za-z0-9]{20,}|(?:AKIA|ASIA)[0-9A-Z]{16})\b|https?:\/\/[^\s/:]+:[^\s/@]+@/i.test(value))
7
+ return true;
8
+ // Require a credential field and an assigned value, not a mention of a sensitive word.
9
+ // Prefix components cover SERVICE_PASSWORD, AWS_SECRET_ACCESS_KEY and vendor TOKEN names.
10
+ const assignment = /\b((?:[a-z][a-z0-9]*[_-])*_?(?:password|passwd|secret|api[_-]?key|access[_-]?token|auth[_-]?token|refresh[_-]?token|session[_-]?token|client[_-]?secret|secret[_-]?(?:access[_-]?)?key|access[_-]?key[_-]?id|private[_-]?key|account[_-]?key|shared[_-]?access[_-]?signature|authorization)|[a-z][a-z0-9]*(?:[_-][a-z0-9]+)*[_-]token|_auth)[ \t]*["']?[ \t]*(:(?!:)|=(?!=|>))[ \t]*(?:"([^"\r\n]*)"|'([^'\r\n]*)'|`([^`\r\n]*)`|([^\s\\"',;]+))/gi;
11
+ for (let match; (match = assignment.exec(value)) !== null;) {
12
+ // An inline Markdown span can end immediately after a field name (`authorization:`).
13
+ // That closing backtick is not the opening quote of a credential value. Resume
14
+ // after it so a later assignment in the same prose is still screened.
15
+ if (match[5] !== undefined && match[2] === ':') {
16
+ const opening = match.index + match[0].indexOf('`');
17
+ const lineStart = value.lastIndexOf('\n', match.index - 1) + 1;
18
+ const preceding = value.slice(lineStart, opening).match(/`+/g) ?? [];
19
+ const spanStart = value.lastIndexOf('`', opening - 1) + 1;
20
+ const heading = value.slice(spanStart, opening);
21
+ if (preceding.filter(run => run.length === 1).length % 2 === 1 && /^[A-Za-z][A-Za-z \t-]*:$/.test(heading)) {
22
+ assignment.lastIndex = opening + 1;
23
+ continue;
24
+ }
25
+ }
26
+ const quoted = match[3] !== undefined || match[4] !== undefined || match[5] !== undefined;
27
+ const token = (match[3] ?? match[4] ?? match[5] ?? match[6]).trim();
28
+ // This exact Core workflow enum describes review semantics, not a credential.
29
+ // Do not exempt other authorization values or the same literal in credential fields.
30
+ if (match[1].toLowerCase() === 'authorization' && token === 'explicit_workflow_and_exact_preview_required')
31
+ continue;
32
+ if (placeholder(token) || /^(?:Bearer|Basic) /i.test(token) && placeholder(token.replace(/^(?:Bearer|Basic) +/i, '')))
33
+ continue;
34
+ // Ordinary type declarations, variable/member references and function calls aren't literals.
35
+ const declaration = /\b(?:const|let|var)\s+$/.test(value.slice(Math.max(0, match.index - 16), match.index));
36
+ if (!quoted && (/^(?:string|number|boolean|unknown|any|never|true|false|password|token|secret)[)}\]]*$/i.test(token) || /^(?:process\.env|os\.environ|config|env|settings)[.\[]/.test(token) || declaration && /^[A-Za-z_$][\w$]*(?:[.(\[]|$)/.test(token)))
37
+ continue;
38
+ if (/^(?:Bearer|Basic)$/i.test(token))
39
+ continue;
40
+ if (token.length >= 6)
41
+ return true;
42
+ }
43
+ // Unquoted HTTP authorization headers have a scheme followed by the credential.
44
+ for (const match of value.matchAll(/\bauthorization[ \t]*:[ \t]*(?:Bearer|Basic)[ \t]+([^\s"',;]+)/gi)) {
45
+ if (match[1].length >= 6 && !placeholder(match[1]))
46
+ return true;
47
+ }
48
+ // Sampling/import boundaries also pass JSON-encoded source strings. Decode valid JSON
49
+ // rather than letting escaped quotes hide an assignment from the same detector.
50
+ if (/^\s*[\[{"]/.test(value)) {
51
+ try {
52
+ const parsed = JSON.parse(value);
53
+ if (typeof parsed === 'string')
54
+ return hasSecret(parsed);
55
+ if (parsed && typeof parsed === 'object')
56
+ return secretStrings(parsed).some(hasSecret);
57
+ }
58
+ catch { /* Ordinary source code is not JSON. */ }
59
+ }
60
+ return false;
61
+ }
62
+ const secretStrings = (input) => typeof input === 'string' ? [input] : Array.isArray(input) ? input.flatMap(secretStrings) : input && typeof input === 'object' ? Object.entries(input).flatMap(([key, v]) => [typeof v === 'string' ? `${key}=${JSON.stringify(v)}` : '', ...secretStrings(v)]) : [];
63
+ export function assertNoSecrets(value) {
64
+ if (secretStrings(value).some(hasSecret))
65
+ throw new Error('Potential credential detected. Remove secrets before storing or sharing Prodero content. No secret value is included in this diagnostic.');
66
+ }
67
+ //# sourceMappingURL=secrets.js.map
@@ -0,0 +1,33 @@
1
+ /** Order and punctuation are evidence. Retrieval's bag-of-words key is not an equality key. */
2
+ export function exactKnowledge(value) { return value.trim().replace(/\s+/g, ' '); }
3
+ const groups = [
4
+ /\b(?:not|never|no|without|cannot|can't|isn't|doesn't)\b/gi,
5
+ /\b(?:enabled|disabled)\b/gi, /\b(?:allowed|forbidden|prohibited)\b/gi,
6
+ /\b(?:before|after)\b/gi, /\b(?:required|optional)\b/gi,
7
+ /\b(?:always|sometimes)\b/gi, /\bv?\d+(?:\.\d+)*\b/gi,
8
+ ];
9
+ const words = (s) => s.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
10
+ export function semanticRisk(a, b) {
11
+ if (exactKnowledge(a) === exactKnowledge(b))
12
+ return false;
13
+ const left = words(a), right = words(b);
14
+ // A bag-of-words match with a different sequence must never imply equality.
15
+ if (left.slice().sort().join(' ') === right.slice().sort().join(' ') && left.join(' ') !== right.join(' '))
16
+ return true;
17
+ if ((/[→←]|->|<-/.test(a + b)) && exactKnowledge(a) !== exactKnowledge(b))
18
+ return true;
19
+ return groups.some((pattern) => {
20
+ const x = [...new Set(a.toLowerCase().match(pattern) ?? [])].sort().join('|');
21
+ const y = [...new Set(b.toLowerCase().match(pattern) ?? [])].sort().join('|');
22
+ return x !== y && Boolean(x || y);
23
+ });
24
+ }
25
+ export function evidenceSentences(content) {
26
+ return content.split(/(?:\r?\n)+|(?<=[.!?])\s+/u).map(exactKnowledge).filter(Boolean);
27
+ }
28
+ /** MVP uses extractive factual bodies: the model can select/compact, but cannot manufacture sentences. */
29
+ export function isExtractivelyGrounded(content, sources) {
30
+ const allowed = new Set(sources.flatMap(evidenceSentences));
31
+ return evidenceSentences(content).every((sentence) => allowed.has(sentence));
32
+ }
33
+ //# sourceMappingURL=semantic-safety.js.map
@@ -0,0 +1,112 @@
1
+ import { z } from "zod";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { parseFrontmatter } from "./format.js";
5
+ import { parseSession } from "./sessions.js";
6
+ import { ensureDerivedStateRoot, exists, projectDataPath, checkedDataPath, parseCheckpointData, sha256, writeFileAtomic } from "./storage.js";
7
+ const EMPTY_CACHE = { version: 3, checkpoints: {}, sessions: {} };
8
+ function stamp(stat) { return { size: Number(stat.size), mtimeMs: Number(stat.mtimeMs), ctimeMs: Number(stat.ctimeMs) }; }
9
+ function sameStamp(left, right) { return Boolean(left && left.size === right.size && left.mtimeMs === right.mtimeMs && left.ctimeMs === right.ctimeMs); }
10
+ const stampSchema = z.object({ size: z.number().nonnegative(), mtimeMs: z.number().finite(), ctimeMs: z.number().finite() });
11
+ const cacheSchema = z.object({
12
+ version: z.literal(3),
13
+ checkpoints: z.record(stampSchema.extend({ id: z.string(), createdAt: z.string(), processedKeys: z.array(z.string()), observationHashes: z.array(z.string()).optional() })),
14
+ sessions: z.record(stampSchema.extend({ id: z.string(), parentSessionId: z.string().regex(/^sess_[a-zA-Z0-9_]+$/).optional(), sha256: z.string().regex(/^[a-f0-9]{64}$/), status: z.enum(["active", "open", "closed"]), hasObservations: z.boolean() })),
15
+ });
16
+ async function readCache(root) {
17
+ await ensureDerivedStateRoot(root);
18
+ const target = await checkedDataPath(root, ".cache", "session-index.json");
19
+ if (!(await exists(target)))
20
+ return { cache: structuredClone(EMPTY_CACHE), state: "missing" };
21
+ try {
22
+ const parsed = cacheSchema.parse(JSON.parse(await fs.readFile(target, "utf8")));
23
+ return { cache: parsed, state: "warm" };
24
+ }
25
+ catch {
26
+ // Disposable derived JSON is rebuilt below; canonical sessions/checkpoints remain authoritative.
27
+ }
28
+ await fs.rm(target, { force: true });
29
+ return { cache: structuredClone(EMPTY_CACHE), state: "invalid" };
30
+ }
31
+ function parseCheckpoint(content, filename) {
32
+ const checkpoint = parseCheckpointData(content, filename);
33
+ return { observationHashes: checkpoint.observationHashes ?? [], id: checkpoint.id, createdAt: checkpoint.createdAt, processedKeys: checkpoint.sessions.map((session) => `${session.path}:${session.sha256}`), size: 0, mtimeMs: 0, ctimeMs: 0 };
34
+ }
35
+ export async function discoverSessions(root, options = {}) {
36
+ const { cache, state } = await readCache(root);
37
+ const nextCheckpoints = {};
38
+ const processed = new Set();
39
+ const processedObservations = new Set();
40
+ let checkpointFilesRead = 0;
41
+ const checkpointDirectory = await checkedDataPath(root, "checkpoints");
42
+ const checkpointNames = (await fs.readdir(checkpointDirectory)).filter((name) => name.endsWith(".json")).sort();
43
+ for (const name of checkpointNames) {
44
+ const target = await checkedDataPath(root, "checkpoints", name);
45
+ const fileStamp = stamp(await fs.stat(target));
46
+ let entry = cache.checkpoints[name];
47
+ if (!sameStamp(entry, fileStamp)) {
48
+ entry = { ...parseCheckpoint(await fs.readFile(target, "utf8"), name), ...fileStamp };
49
+ checkpointFilesRead += 1;
50
+ }
51
+ nextCheckpoints[name] = entry;
52
+ for (const hash of entry.observationHashes ?? [])
53
+ processedObservations.add(hash);
54
+ for (const key of entry.processedKeys)
55
+ processed.add(key);
56
+ }
57
+ const nextSessions = {};
58
+ const sessions = [];
59
+ let sessionBodiesRead = 0;
60
+ const sessionDirectory = await checkedDataPath(root, "sessions");
61
+ const sessionNames = (await fs.readdir(sessionDirectory)).filter((name) => name.endsWith(".md") && !name.startsWith(".")).sort();
62
+ for (const name of sessionNames) {
63
+ const target = await checkedDataPath(root, "sessions", name);
64
+ const fileStamp = stamp(await fs.stat(target));
65
+ let cached = cache.sessions[name];
66
+ let parsed;
67
+ if (!sameStamp(cached, fileStamp)) {
68
+ const content = await fs.readFile(target, "utf8");
69
+ sessionBodiesRead += 1;
70
+ parsed = parseSession(content, name);
71
+ if (name.startsWith("sess_")) {
72
+ const { metadata } = parseFrontmatter(content);
73
+ if (metadata.id !== path.basename(name, ".md") || !["open", "active", "closed"].includes(String(metadata.status)) || typeof metadata.created_at !== "string") {
74
+ throw new Error(`Invalid Prodero session: ${name} metadata is missing, malformed, or does not match the filename.`);
75
+ }
76
+ }
77
+ cached = {
78
+ ...fileStamp,
79
+ id: parsed.id,
80
+ parentSessionId: parsed.parentSessionId,
81
+ sha256: sha256(content),
82
+ status: parsed.status,
83
+ hasObservations: Object.values(parsed.sections).some((items) => items.length > 0),
84
+ };
85
+ }
86
+ nextSessions[name] = cached;
87
+ const pending = cached.hasObservations && !processed.has(`${name}:${cached.sha256}`);
88
+ const needsBody = options.loadPendingBodies && pending && (!options.onlyClosed || cached.status === "closed");
89
+ if (needsBody && !parsed) {
90
+ parsed = parseSession(await fs.readFile(target, "utf8"), name);
91
+ sessionBodiesRead += 1;
92
+ }
93
+ sessions.push({ id: cached.id, parentSessionId: cached.parentSessionId, path: name, sha256: cached.sha256, status: cached.status, hasObservations: cached.hasObservations, pending, session: parsed });
94
+ }
95
+ const nextCache = { version: 3, checkpoints: nextCheckpoints, sessions: nextSessions };
96
+ await ensureDerivedStateRoot(root);
97
+ await writeFileAtomic(projectDataPath(root, ".cache", "session-index.json"), `${JSON.stringify(nextCache)}\n`);
98
+ const checkpointEntries = Object.values(nextCheckpoints).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
99
+ return {
100
+ processedObservations,
101
+ processedSessionPaths: new Set([...processed].map(key => key.slice(0, key.lastIndexOf(":")))),
102
+ sessions,
103
+ checkpointCount: checkpointEntries.length,
104
+ latestCheckpointId: checkpointEntries.at(-1)?.id ?? null,
105
+ lastSyncAt: checkpointEntries.at(-1)?.createdAt ?? null,
106
+ sessionFilesScanned: sessionNames.length,
107
+ sessionBodiesRead,
108
+ checkpointFilesRead,
109
+ cacheState: state === "warm" && (sessionBodiesRead > 0 || checkpointFilesRead > 0) ? "stale" : state,
110
+ };
111
+ }
112
+ //# sourceMappingURL=session-index.js.map