@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,288 @@
1
+ import { ownsSyncLock } from './sync-transaction.js';
2
+ /** Read-only, deterministic candidate discovery. No transformation or lifecycle mutation. */
3
+ import { loadMemoryCatalog } from './memory-index.js';
4
+ import { getMemoryPressureSnapshot } from './memory-pressure.js';
5
+ import { provenanceEdges, readKnowledgeInventory } from './knowledge-provenance.js';
6
+ import { assertCompatibleProject, checkedDataPath, exists, sha256 } from './storage.js';
7
+ export const MEMORY_INTELLIGENCE_POLICY = {
8
+ version: 1, limits: { candidates: 20, records: 24, evidence: 32, sessions: 8, pairs: 24 },
9
+ thresholds: { repeatedSessions: 2, fragmentationRecords: 4, repeatedCheckpoints: 2 },
10
+ scoring: {
11
+ context_pressure: { perRepeatedRecord: 12, recordCap: 5, perSession: 3, sessionCap: 8 },
12
+ fragmentation: { perRecordBeyondTwo: 2, recordCap: 6 },
13
+ recent_evolution: { perCreatedRecord: 2, perTransition: 3, perRefinement: 2, cap: 12 },
14
+ potential_conflict: { perPair: 20, pairCap: 2 },
15
+ redundancy: { perCheckpoint: 2, checkpointCap: 4 },
16
+ unresolved: { perCheckpoint: 3, checkpointCap: 4 },
17
+ },
18
+ };
19
+ const compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
20
+ const sorted = (values) => [...new Set(values)].sort(compare);
21
+ const pairKey = (a, b) => JSON.stringify([a, b].sort(compare));
22
+ const budget = (r) => r.reason === 'context-budget' || r.reason === 'required-budget';
23
+ const useful = (r) => r.bucket !== 'filtered' && ['selected', 'context-budget', 'required-budget', 'item-limit', 'rank-window'].includes(r.reason);
24
+ const coUsable = (r) => r.bucket === 'contextual' && (r.reason === 'selected' || budget(r));
25
+ const protectedCategories = new Set(['instruction', 'principle', 'invariant', 'constraint', 'decision']);
26
+ /** No writer locks, migration, recovery, caches or telemetry writes on this path. */
27
+ async function assertQuiescent(root) {
28
+ for (const relative of ['.transactions/pending.json', '.transactions/migration.json', '.transactions/lock'])
29
+ if (!(relative === '.transactions/lock' && ownsSyncLock(root)) && await exists(await checkedDataPath(root, relative)))
30
+ throw new Error('Memory intelligence requires a stable project: a storage writer or recovery journal is pending. No recovery was attempted.');
31
+ }
32
+ async function computeMemoryIntelligencePlan(root, selectedId) {
33
+ let selectedSourceIds = [];
34
+ await assertCompatibleProject(root);
35
+ await assertQuiescent(root);
36
+ const catalog = await loadMemoryCatalog(root, { persistCache: false });
37
+ const snapshot = await getMemoryPressureSnapshot(root);
38
+ // Reuse archive/provenance reading; cached current summaries are not reread as bodies.
39
+ const archived = await readKnowledgeInventory(root, { storage: 'archive' });
40
+ const allLayers = new Map(catalog.summaries.map(r => [r.id, r.layer]));
41
+ for (const r of archived) {
42
+ if (allLayers.has(r.item.id))
43
+ throw new Error(`Duplicate knowledge ID across current/archive storage: ${r.item.id}`);
44
+ allLayers.set(r.item.id, r.layer);
45
+ }
46
+ const edges = provenanceEdges([...catalog.summaries, ...archived.map(r => r.item)]);
47
+ const retired = new Set(edges.map(e => e.to));
48
+ // Disputed claims remain visible for conflict diagnostics, not ordinary retrieval.
49
+ const current = catalog.summaries.filter(r => ['active', 'disputed'].includes(r.status) && !retired.has(r.id)).sort((a, b) => compare(a.id, b.id));
50
+ const byId = new Map(current.map(r => [r.id, r]));
51
+ const sessions = [...new Map(snapshot.sessions.map(s => [s.sessionId, s])).values()].sort((a, b) => compare(a.sessionId, b.sessionId));
52
+ const syncs = [...new Map(snapshot.syncs.map(s => [s.checkpoint, s])).values()].sort((a, b) => compare(a.checkpoint, b.checkpoint));
53
+ const sessionsFor = new Map();
54
+ let ignoredRetrievalDetails = 0;
55
+ for (const session of sessions) {
56
+ const records = new Map();
57
+ for (const r of session.records) {
58
+ if (byId.get(r.id)?.layer !== r.layer || !useful(r)) {
59
+ ignoredRetrievalDetails++;
60
+ continue;
61
+ }
62
+ records.set(r.id, r);
63
+ }
64
+ sessionsFor.set(session.sessionId, records);
65
+ }
66
+ const direct = new Set();
67
+ for (const r of current)
68
+ for (const target of [...r.related, ...r.relations.filter(e => ['related_to', 'depends_on', 'implements', 'contradicts'].includes(e.type)).map(e => e.target)])
69
+ if (byId.has(target) && target !== r.id)
70
+ direct.add(pairKey(r.id, target));
71
+ const parents = new Map();
72
+ const successors = new Map();
73
+ for (const e of edges) {
74
+ const sources = parents.get(e.from) ?? new Set();
75
+ sources.add(e.to);
76
+ parents.set(e.from, sources);
77
+ const newer = successors.get(e.to) ?? new Set();
78
+ newer.add(e.from);
79
+ successors.set(e.to, newer);
80
+ }
81
+ const resolve = (id) => {
82
+ if (byId.has(id))
83
+ return id;
84
+ const seen = new Set(), todo = [id], matches = new Set();
85
+ for (let i = 0; i < todo.length; i++) {
86
+ const next = todo[i];
87
+ if (seen.has(next))
88
+ continue;
89
+ seen.add(next);
90
+ if (byId.has(next))
91
+ matches.add(next);
92
+ else
93
+ todo.push(...(successors.get(next) ?? []));
94
+ }
95
+ return matches.size === 1 ? [...matches][0] : null;
96
+ };
97
+ // Observation refs are not knowledge IDs. Resolve them only via existing derived_from,
98
+ // loading only current records named by bounded committed Sync changes.
99
+ const sourceIds = new Map();
100
+ const requiredSources = new Set(syncs.flatMap(s => s.reconciliation.map(e => e.source)));
101
+ for (const id of sorted(syncs.flatMap(s => s.changes.map(c => c.id)))) {
102
+ if (!byId.has(id))
103
+ continue;
104
+ const item = await catalog.load(id);
105
+ for (const source of item.derivedFrom)
106
+ if (requiredSources.has(source)) {
107
+ const ids = sourceIds.get(source) ?? new Set();
108
+ ids.add(id);
109
+ sourceIds.set(source, ids);
110
+ }
111
+ }
112
+ const events = new Map();
113
+ let ignoredReconciliation = 0;
114
+ for (const sync of syncs)
115
+ for (const e of sync.reconciliation) {
116
+ const targetId = resolve(e.target.id);
117
+ if (!targetId || allLayers.has(e.target.id) && allLayers.get(e.target.id) !== e.target.layer) {
118
+ ignoredReconciliation++;
119
+ continue;
120
+ }
121
+ const sources = sourceIds.get(e.source);
122
+ const sourceId = byId.has(e.source) ? e.source : sources?.size === 1 ? [...sources][0] : null;
123
+ const event = { checkpoint: sync.checkpoint, source: e.source, sourceId, targetId, originalTargetId: e.target.id, classification: e.classification,
124
+ verifiedCurrentConflict: e.classification === 'potential_conflict' && sourceId !== null && sourceId !== targetId && targetId === e.target.id };
125
+ events.set(JSON.stringify([sync.checkpoint, e.source, e.target.id, e.classification]), event);
126
+ }
127
+ const evidence = [...events.values()].sort((a, b) => compare(JSON.stringify(a), JSON.stringify(b)));
128
+ const pairs = new Map();
129
+ const pair = (a, b) => {
130
+ const key = pairKey(a, b);
131
+ let p = pairs.get(key);
132
+ if (!p) {
133
+ p = { ids: [a, b].sort(compare), sessions: new Set(), coOmissionSessions: new Set(), reasons: new Set() };
134
+ pairs.set(key, p);
135
+ }
136
+ return p;
137
+ };
138
+ const affinity = (a, b) => {
139
+ if (direct.has(pairKey(a.id, b.id)))
140
+ return 'explicit_relationship';
141
+ if (a.scopeRef && a.scope === b.scope && a.scopeRef === b.scopeRef && (a.scopeKind ?? null) === (b.scopeKind ?? null))
142
+ return 'same_named_scope';
143
+ if ([...(parents.get(a.id) ?? [])].some(id => parents.get(b.id)?.has(id)))
144
+ return 'shared_provenance_source';
145
+ return null;
146
+ };
147
+ for (const [sessionId, records] of sessionsFor) {
148
+ const contextual = [...records.values()].filter(coUsable).sort((a, b) => compare(a.id, b.id));
149
+ for (let i = 0; i < contextual.length; i++)
150
+ for (let j = i + 1; j < contextual.length; j++) {
151
+ const a = contextual[i], b = contextual[j], reason = affinity(byId.get(a.id), byId.get(b.id));
152
+ if (!reason)
153
+ continue;
154
+ const p = pair(a.id, b.id);
155
+ p.sessions.add(sessionId);
156
+ p.reasons.add(reason);
157
+ if (budget(a) && budget(b))
158
+ p.coOmissionSessions.add(sessionId);
159
+ }
160
+ }
161
+ const admissible = new Map([...pairs].filter(([, p]) => p.sessions.size >= MEMORY_INTELLIGENCE_POLICY.thresholds.repeatedSessions));
162
+ // A verified incompatible pair is explicit evidence, not a lexical grouping guess.
163
+ for (const e of evidence)
164
+ if (e.verifiedCurrentConflict) {
165
+ const p = pair(e.sourceId, e.targetId);
166
+ p.reasons.add('verified_reconciliation_conflict');
167
+ admissible.set(pairKey(...p.ids), p);
168
+ }
169
+ const observed = sorted([...sessionsFor.values()].flatMap(m => [...m.keys()]).concat(evidence.flatMap(e => [e.targetId, ...(e.verifiedCurrentConflict ? [e.sourceId] : [])])));
170
+ const unassigned = new Set(observed), areas = [];
171
+ // Complete-link partition: a bridge cannot merge unrelated ends of a chain.
172
+ for (const seed of observed) {
173
+ if (!unassigned.has(seed))
174
+ continue;
175
+ const area = [seed];
176
+ unassigned.delete(seed);
177
+ for (const id of observed)
178
+ if (unassigned.has(id) && area.every(member => admissible.has(pairKey(member, id)))) {
179
+ area.push(id);
180
+ unassigned.delete(id);
181
+ }
182
+ areas.push(area);
183
+ }
184
+ const L = MEMORY_INTELLIGENCE_POLICY.limits, W = MEMORY_INTELLIGENCE_POLICY.scoring;
185
+ const candidates = areas.flatMap(ids => {
186
+ const members = new Set(ids), records = ids.map(id => byId.get(id));
187
+ const considered = new Set(), selected = new Set(), omitted = new Set();
188
+ const omissionByRecord = new Map(), selectedRecords = new Set();
189
+ const retrieval = [];
190
+ for (const session of sessions) {
191
+ const relevant = [...(sessionsFor.get(session.sessionId)?.values() ?? [])].filter(r => members.has(r.id));
192
+ if (!relevant.length)
193
+ continue;
194
+ considered.add(session.sessionId);
195
+ const selectedIds = sorted(relevant.filter(r => r.reason === 'selected').map(r => r.id));
196
+ if (selectedIds.length)
197
+ selected.add(session.sessionId);
198
+ for (const id of selectedIds)
199
+ selectedRecords.add(id);
200
+ const omittedRecords = relevant.filter(budget);
201
+ if (omittedRecords.length)
202
+ omitted.add(session.sessionId);
203
+ for (const r of omittedRecords) {
204
+ const set = omissionByRecord.get(r.id) ?? new Set();
205
+ set.add(session.sessionId);
206
+ omissionByRecord.set(r.id, set);
207
+ }
208
+ retrieval.push({ sessionId: session.sessionId, selectedIds, contextOmittedIds: sorted(omittedRecords.filter(r => r.reason === 'context-budget').map(r => r.id)), requiredOmittedIds: sorted(omittedRecords.filter(r => r.reason === 'required-budget').map(r => r.id)), omittedTokens: omittedRecords.reduce((n, r) => n + r.estimatedTokens, 0), contextBudgetTokens: session.budgets.contextBudgetTokens, requiredBudgetTokens: session.budgets.requiredContextBudgetTokens });
209
+ }
210
+ const repeated = [...omissionByRecord].filter(([, s]) => s.size >= 2).map(([id]) => id).sort(compare);
211
+ const areaEvents = evidence.filter(e => members.has(e.targetId));
212
+ const classifications = {};
213
+ for (const e of areaEvents)
214
+ classifications[e.classification] = (classifications[e.classification] ?? 0) + 1;
215
+ const conflictPairs = new Set(areaEvents.filter(e => e.verifiedCurrentConflict && members.has(e.sourceId)).map(e => pairKey(e.sourceId, e.targetId)));
216
+ const unverifiedConflicts = areaEvents.filter(e => e.classification === 'potential_conflict' && !e.verifiedCurrentConflict).length;
217
+ const checkpoints = (types) => new Set(areaEvents.filter(e => types.includes(e.classification)).map(e => e.checkpoint));
218
+ const redundant = checkpoints(['duplicate', 'supporting']), unresolved = checkpoints(['unresolved']);
219
+ const additions = new Set(), updates = new Set();
220
+ const syncDetails = syncs.flatMap(s => s.changes.filter(c => members.has(c.id) && byId.get(c.id)?.layer === c.layer).map(c => {
221
+ if (c.actions.includes('created'))
222
+ additions.add(c.id);
223
+ if (c.actions.some(a => a === 'updated' || a === 'merged'))
224
+ updates.add(c.id);
225
+ return { checkpoint: s.checkpoint, id: c.id, actions: c.actions };
226
+ }));
227
+ const connections = [...admissible.values()].filter(p => p.ids.every(id => members.has(id))).sort((a, b) => compare(pairKey(...a.ids), pairKey(...b.ids)));
228
+ const repeatedCoRecords = new Set(connections.filter(p => p.sessions.size >= 2).flatMap(p => p.ids));
229
+ const scores = { context_pressure: 0, fragmentation: 0, recent_evolution: 0, potential_conflict: 0, redundancy: 0, unresolved: 0 };
230
+ if (repeated.length)
231
+ scores.context_pressure = W.context_pressure.perRepeatedRecord * Math.min(W.context_pressure.recordCap, repeated.length) + W.context_pressure.perSession * Math.min(W.context_pressure.sessionCap, omitted.size);
232
+ if (repeatedCoRecords.size >= MEMORY_INTELLIGENCE_POLICY.thresholds.fragmentationRecords && records.filter(r => !protectedCategories.has(r.category) && repeatedCoRecords.has(r.id)).length >= 2)
233
+ scores.fragmentation = W.fragmentation.perRecordBeyondTwo * Math.min(W.fragmentation.recordCap, repeatedCoRecords.size - 2);
234
+ scores.potential_conflict = W.potential_conflict.perPair * Math.min(W.potential_conflict.pairCap, conflictPairs.size);
235
+ if (redundant.size >= 2)
236
+ scores.redundancy = W.redundancy.perCheckpoint * Math.min(W.redundancy.checkpointCap, redundant.size);
237
+ if (unresolved.size >= 2)
238
+ scores.unresolved = W.unresolved.perCheckpoint * Math.min(W.unresolved.checkpointCap, unresolved.size);
239
+ if (!Object.values(scores).some(n => n > 0))
240
+ return [];
241
+ scores.recent_evolution = Math.min(W.recent_evolution.cap, W.recent_evolution.perCreatedRecord * additions.size + W.recent_evolution.perTransition * (classifications.state_transition ?? 0) + W.recent_evolution.perRefinement * (classifications.refinement ?? 0));
242
+ const categories = {};
243
+ for (const r of records)
244
+ categories[r.category] = (categories[r.category] ?? 0) + 1;
245
+ const named = records[0].scopeRef, sameScope = !!named && records.every(r => r.scope === records[0].scope && r.scopeRef === named && r.scopeKind === records[0].scopeKind);
246
+ const recordDetails = records.slice(0, L.records).map(r => ({ id: r.id, layer: r.layer, title: r.title.slice(0, 160), scope: r.scope, scopeRef: r.scopeRef?.slice(0, 256) ?? null, scopeKind: r.scopeKind ?? null, category: r.category, status: r.status, estimatedTokens: r.estimatedTokens, protected: protectedCategories.has(r.category), budgetOmissionSessions: omissionByRecord.get(r.id)?.size ?? 0 }));
247
+ const clipIds = (values) => ({ ids: values.slice(0, L.records), dropped: Math.max(0, values.length - L.records) });
248
+ const areaId = `area_${sha256(JSON.stringify(ids)).slice(0, 20)}`;
249
+ if (areaId === selectedId)
250
+ selectedSourceIds = ids;
251
+ return [{ id: areaId, label: sameScope ? named.slice(0, 160) : records[0].title.slice(0, 160), recommendation: 'transformation_candidate',
252
+ recordCount: ids.length, layers: { local: records.filter(r => r.layer === 'local').length, versioned: records.filter(r => r.layer === 'versioned').length }, categories, containsProtectedKnowledge: records.some(r => protectedCategories.has(r.category)), records: recordDetails,
253
+ signals: Object.keys(scores).filter(s => scores[s] > 0), priority: { total: Object.values(scores).reduce((a, b) => a + b, 0), components: scores },
254
+ counts: { consideredSessions: considered.size, selectedSessions: selected.size, selectedRecords: selectedRecords.size, budgetOmittedRecords: omissionByRecord.size, repeatedBudgetOmittedRecords: repeated.length, budgetOmissionSessions: omitted.size, repeatedCoRecords: repeatedCoRecords.size, recentCreatedRecords: additions.size, recentUpdatedRecords: updates.size, verifiedConflictPairs: conflictPairs.size, unverifiedConflictEvidence: unverifiedConflicts, redundancyCheckpoints: redundant.size, unresolvedCheckpoints: unresolved.size },
255
+ reconciliation: classifications,
256
+ evidence: { repeatedBudgetOmitted: clipIds(repeated), retrieval: retrieval.slice(-L.sessions).map(r => ({ ...r, selectedIds: clipIds(r.selectedIds), contextOmittedIds: clipIds(r.contextOmittedIds), requiredOmittedIds: clipIds(r.requiredOmittedIds) })),
257
+ relationships: connections.slice(0, L.pairs).map(p => ({ ids: p.ids, reasons: sorted(p.reasons), coRetrievalSessions: p.sessions.size, coOmissionSessions: p.coOmissionSessions.size, sessionIds: sorted(p.sessions).slice(-L.sessions), sessionIdsDropped: Math.max(0, p.sessions.size - L.sessions) })),
258
+ reconciliation: areaEvents.slice(-L.evidence), sync: syncDetails.slice(-L.evidence) },
259
+ truncated: { records: Math.max(0, ids.length - L.records), retrievalSessions: Math.max(0, retrieval.length - L.sessions), relationships: Math.max(0, connections.length - L.pairs), reconciliation: Math.max(0, areaEvents.length - L.evidence), sync: Math.max(0, syncDetails.length - L.evidence) } }];
260
+ }).sort((a, b) => b.priority.total - a.priority.total || compare(a.id, b.id));
261
+ await assertQuiescent(root);
262
+ const plan = { version: 1, policy: MEMORY_INTELLIGENCE_POLICY, coverage: { telemetry: snapshot.status, checkpointCoverage: snapshot.checkpointCoverage, ...snapshot.coverage,
263
+ syncChangesDropped: syncs.reduce((n, s) => n + s.changesDropped, 0), reconciliationDetailsDropped: syncs.reduce((n, s) => n + s.relationshipsDropped, 0),
264
+ currentRecords: current.length, archivedRecordsExcluded: archived.length, historicalOrRetiredRecordsExcluded: catalog.summaries.length - current.length, ignoredRetrievalDetails, ignoredReconciliation, unverifiedConflictEvidence: evidence.filter(e => e.classification === 'potential_conflict' && !e.verifiedCurrentConflict).length },
265
+ candidateCount: candidates.length, candidates: candidates.slice(0, L.candidates), truncated: { candidates: Math.max(0, candidates.length - L.candidates) } };
266
+ const sources = new Set(selectedSourceIds);
267
+ const predecessors = new Set(edges.filter(e => sources.has(e.from)).map(e => e.to));
268
+ const provenance = edges.filter(e => sources.has(e.from) || sources.has(e.to) || predecessors.has(e.from) || predecessors.has(e.to));
269
+ const referenced = new Set([...selectedSourceIds, ...provenance.flatMap(e => [e.from, e.to])]);
270
+ const locations = [...catalog.summaries.map(r => ({ id: r.id, path: r.relativePath, layer: r.layer, storage: 'current' })),
271
+ ...archived.map(r => ({ id: r.item.id, path: r.path, layer: r.layer, storage: 'archive' }))].filter(r => referenced.has(r.id)).sort((a, b) => compare(a.id, b.id));
272
+ return { plan, selectedSourceIds, provenance, locations };
273
+ }
274
+ export async function getMemoryIntelligencePlan(root) {
275
+ return (await computeMemoryIntelligencePlan(root)).plan;
276
+ }
277
+ /** Internal exact membership, including IDs omitted from the bounded diagnostic table. */
278
+ export async function getMemoryIntelligenceCandidate(root, candidateId) {
279
+ if (!/^area_[a-f0-9]{20}$/.test(candidateId))
280
+ throw new Error('Invalid memory candidate ID');
281
+ const result = await computeMemoryIntelligencePlan(root, candidateId);
282
+ const candidate = result.plan.candidates.find(c => c.id === candidateId);
283
+ if (!candidate)
284
+ throw new Error('Memory candidate is stale or unavailable in the current bounded plan');
285
+ const value = { candidate, sourceIds: result.selectedSourceIds, policy: result.plan.policy, coverage: result.plan.coverage, provenance: result.provenance, locations: result.locations };
286
+ return { ...value, revision: sha256(JSON.stringify(value)) };
287
+ }
288
+ //# sourceMappingURL=memory-intelligence.js.map
@@ -0,0 +1,66 @@
1
+ /** Explicit Sync review of dangling canonical links. Never guesses replacement knowledge. */
2
+ import fs from 'node:fs/promises';
3
+ import { loadKnowledgeGraph } from './knowledge-graph.js';
4
+ import { readKnowledgeInventory } from './knowledge-provenance.js';
5
+ import { renderKnowledge } from './memory.js';
6
+ import { listCheckpoints, sha256 } from './storage.js';
7
+ import { commitSyncTransaction, ownsSyncLock } from './sync-transaction.js';
8
+ const digest = (value) => sha256(JSON.stringify(value));
9
+ export async function previewMemoryLinkRepair(root, createdAt = new Date().toISOString()) {
10
+ const graph = await loadKnowledgeGraph(root, undefined, { persistCache: false });
11
+ const broken = new Set(graph.brokenReferences);
12
+ const inventory = await readKnowledgeInventory(root);
13
+ // Bound each exact review; remaining links are offered by the next Sync.
14
+ const changes = inventory.filter(r => r.storage === 'current').flatMap(r => {
15
+ const links = new Map();
16
+ const missing = (type, target) => {
17
+ if (!broken.has(`${r.item.id}:${type}:${target}`))
18
+ return false;
19
+ links.set(`${type}:${target}`, { type, target });
20
+ return true;
21
+ };
22
+ const after = { ...r.item, version: 3, applicability: r.item.applicability ?? { kind: 'legacy' }, updatedAt: createdAt,
23
+ supersedes: r.item.supersedes.filter(target => !missing('supersedes', target)),
24
+ related: r.item.related.filter(target => !missing('related_to', target)),
25
+ relations: (r.item.relations ?? []).filter(link => !missing(link.type, link.target)), };
26
+ return links.size ? [{ id: r.item.id, title: r.item.title, layer: r.layer, path: r.path,
27
+ before: r.revision, original: r.item, after: renderKnowledge(after), links: [...links.values()] }] : [];
28
+ }).slice(0, 100);
29
+ const count = changes.reduce((n, c) => n + c.links.length, 0);
30
+ const payload = { root: await fs.realpath(root), createdAt, changes, count,
31
+ remaining: broken.size - count, requiresVersionedReview: changes.some(c => c.layer === 'versioned') };
32
+ return { ...payload, previewId: digest(payload) };
33
+ }
34
+ /** Called only under Sync's lock, after its exact review approval has been persisted. */
35
+ export async function applyMemoryLinkRepair(root, preview, approved) {
36
+ if (!ownsSyncLock(root) || !approved)
37
+ throw new Error('Link repair requires a locked, approved exact review');
38
+ const { previewId, ...payload } = preview;
39
+ if (digest(payload) !== previewId || preview.root !== await fs.realpath(root))
40
+ throw new Error('Link repair preview mismatch');
41
+ const checkpoint = `chk_link_repair_${previewId}`;
42
+ const checkpoints = await listCheckpoints(root);
43
+ const receipt = checkpoints.find(c => c.id === checkpoint);
44
+ if (receipt) {
45
+ if (receipt.note !== `Approved link repair ${previewId}`)
46
+ throw new Error('Link repair receipt mismatch');
47
+ }
48
+ else {
49
+ const fresh = await previewMemoryLinkRepair(root, preview.createdAt);
50
+ if (fresh.previewId !== previewId)
51
+ throw new Error('Stale link repair preview; knowledge changed');
52
+ const record = { id: checkpoint, createdAt: preview.createdAt,
53
+ previousCheckpoint: checkpoints.at(-1)?.id ?? null, sessions: [], consolidation: 'not-performed',
54
+ knowledgeBefore: preview.changes.map(c => c.original), createdMemory: [],
55
+ updatedMemory: preview.changes.map(c => c.id), supersededMemory: [], duplicateMemory: [], conflicts: [],
56
+ summary: { sessions: 0, candidates: 0, created: 0, consolidated: 0, duplicates: 0, superseded: 0, conflicts: 0 },
57
+ note: `Approved link repair ${previewId}` };
58
+ await commitSyncTransaction(root, [
59
+ ...preview.changes.map(c => ({ path: c.path, before: c.before, after: c.after })),
60
+ { path: `checkpoints/${checkpoint}.json`, before: null, after: JSON.stringify(record, null, 2) + '\n' },
61
+ ], { policyMode: 'exact' });
62
+ }
63
+ return { checkpoint, fixed: preview.count, remaining: preview.remaining,
64
+ revisions: preview.changes.map(c => ({ path: c.path, hash: sha256(c.after) })) };
65
+ }
66
+ //# sourceMappingURL=memory-link-repair.js.map
@@ -0,0 +1,122 @@
1
+ import { getKnowledgeProvenance } from "./knowledge-provenance.js";
2
+ import { isPathScope, pathApplies } from "./path-scope.js";
3
+ import { assertNoSecrets } from "./secrets.js";
4
+ import { buildIndex } from "./index.js";
5
+ import { sortableId } from "./ids.js";
6
+ import { assertEnglishProse } from "./language.js";
7
+ import { loadMemoryCatalog } from "./memory-index.js";
8
+ import { renderKnowledge } from "./memory.js";
9
+ import { normalized, tokens } from "./matching.js";
10
+ import { listCheckpoints, sha256 } from "./storage.js";
11
+ import { commitSyncTransaction, fileHash, recoverSyncTransaction, safeProjectPath, withSyncLock } from "./sync-transaction.js";
12
+ const CURRENT = new Set(["active", "disputed"]);
13
+ export async function searchMemoryPage(root, query, options = {}) {
14
+ const catalog = await loadMemoryCatalog(root);
15
+ const queryTokens = tokens(query);
16
+ const path = options.path?.replace(/^\.\//, "").replace(/\/$/, "");
17
+ const statuses = options.includeHistorical ? null : CURRENT;
18
+ const shadowed = new Set(catalog.summaries.filter((item) => CURRENT.has(item.status)).flatMap((item) => item.supersedes));
19
+ const matches = catalog.summaries
20
+ .filter((item) => !statuses || statuses.has(item.status))
21
+ .filter((item) => options.includeHistorical || options.includeShadowed || !shadowed.has(item.id))
22
+ .filter((item) => !options.category || item.category === options.category)
23
+ .filter((item) => !options.layer || item.layer === options.layer)
24
+ .filter((item) => options.scope !== "path" || (item.applicability?.kind === "scoped" && item.applicability.paths ? item.applicability.paths.some(p => Boolean(path && pathApplies(p.replace(/\/\*\*$/, ""), path))) : isPathScope(item) && Boolean(path && pathApplies(item.scopeRef, path))))
25
+ .filter((item) => options.scope !== "global" || item.scope === "global")
26
+ .filter((item) => options.scope !== "project" || item.scope === "global")
27
+ .map((item) => {
28
+ const overlap = item.tokens.filter((token) => queryTokens.has(token)).length;
29
+ const titleOverlap = [...tokens(item.title)].filter((token) => queryTokens.has(token)).length;
30
+ const exactTitle = normalized(item.title) === normalized(query);
31
+ const exactContent = item.normalizedContent === normalized(query);
32
+ const score = overlap * 10 + titleOverlap * 20 + (exactTitle ? 1_000 : 0) + (exactContent ? 2_000 : 0);
33
+ return { item, score };
34
+ })
35
+ .filter(({ score }) => !query.trim() || score > 0)
36
+ .sort((a, b) => b.score - a.score || a.item.id.localeCompare(b.item.id));
37
+ const offset = Math.max(0, options.offset ?? 0), limit = Math.min(20, Math.max(1, options.limit ?? 8));
38
+ const page = matches.slice(offset, offset + limit);
39
+ return { total: matches.length, offset, nextOffset: offset + limit < matches.length ? offset + limit : null,
40
+ matches: await Promise.all(page.map(async ({ item, score }) => ({ ...(await catalog.load(item.id)), layer: item.layer, score, shadowed: shadowed.has(item.id), revision: catalog.originalHashes.get(item.id) }))) };
41
+ }
42
+ export async function searchMemory(root, query, options = {}) {
43
+ if (!query.trim())
44
+ return []; // Empty conversational searches must never select an arbitrary forget target.
45
+ return (await searchMemoryPage(root, query, options)).matches;
46
+ }
47
+ export async function getMemory(root, id) {
48
+ if (!/^[a-zA-Z0-9_-]+$/.test(id))
49
+ throw new Error("Invalid Prodero memory id.");
50
+ const catalog = await loadMemoryCatalog(root);
51
+ return catalog.summaries.some(item => item.id === id) ? catalog.load(id) : (await getKnowledgeProvenance(root, id)).item;
52
+ }
53
+ export async function getMemoryLayer(root, id) {
54
+ if (!/^[a-zA-Z0-9_-]+$/.test(id))
55
+ throw new Error("Invalid Prodero memory id.");
56
+ const entry = (await loadMemoryCatalog(root)).summaries.find((item) => item.id === id);
57
+ if (!entry)
58
+ return (await getKnowledgeProvenance(root, id)).layer;
59
+ return entry.layer;
60
+ }
61
+ export async function forgetMemory(root, input) {
62
+ assertNoSecrets(input);
63
+ return withSyncLock(root, async () => {
64
+ await recoverSyncTransaction(root);
65
+ assertEnglishProse([input.reason], "Forgotten Prodero knowledge metadata");
66
+ let item;
67
+ if (input.id)
68
+ item = await getMemory(root, input.id);
69
+ else {
70
+ const matches = await searchMemory(root, input.query ?? "", { limit: 3 });
71
+ if (matches.length !== 1)
72
+ throw new Error(matches.length ? `Forget is ambiguous. Search first and pass one exact id: ${matches.map((match) => match.id).join(", ")}` : "No active knowledge matched. Search first or pass an exact id.");
73
+ item = matches[0];
74
+ }
75
+ if (await getMemoryLayer(root, item.id) === "versioned")
76
+ throw new Error("Versioned Knowledge cannot be forgotten through local memory operations. Use explicit repository maintenance and review the Git diff.");
77
+ if (input.revision) {
78
+ const actual = await fileHash(await safeProjectPath(root, `local/memory/${item.id}.md`));
79
+ if (actual !== input.revision)
80
+ throw new Error("Knowledge changed since it was opened. Reload and review the current revision.");
81
+ }
82
+ if (!CURRENT.has(item.status))
83
+ return { forgotten: false, alreadyHistorical: true, item };
84
+ const now = new Date();
85
+ const checkpointId = sortableId("chk", now);
86
+ const layer = await getMemoryLayer(root, item.id);
87
+ const relative = layer === "versioned" ? `memory/${item.id}.md` : `local/memory/${item.id}.md`;
88
+ const before = await fileHash(await safeProjectPath(root, relative));
89
+ if (!before)
90
+ throw new Error(`Canonical Prodero memory ${item.id} was not found.`);
91
+ const updated = {
92
+ ...item,
93
+ status: "deprecated",
94
+ updatedAt: now.toISOString(),
95
+ derivedFrom: [...new Set([...item.derivedFrom, `user:forget:${checkpointId}`])].sort(),
96
+ evidence: [...(item.evidence ?? []), { kind: "user_instruction", ref: input.reason?.trim() || "Explicit conversational forget" }],
97
+ };
98
+ const checkpoints = await listCheckpoints(root);
99
+ const checkpoint = {
100
+ id: checkpointId,
101
+ createdAt: now.toISOString(),
102
+ previousCheckpoint: checkpoints.at(-1)?.id ?? null,
103
+ sessions: [],
104
+ consolidation: "not-performed",
105
+ createdMemory: [],
106
+ createdSkills: [],
107
+ updatedMemory: [item.id],
108
+ supersededMemory: [],
109
+ duplicateMemory: [],
110
+ conflicts: [],
111
+ summary: { sessions: 0, candidates: 0, created: 0, consolidated: 0, duplicates: 0, superseded: 0, conflicts: 0 },
112
+ note: `Explicit conversational forget: ${item.id}`,
113
+ };
114
+ await commitSyncTransaction(root, [
115
+ { path: relative, before, after: renderKnowledge(updated) },
116
+ { path: `checkpoints/${checkpointId}.json`, before: null, after: `${JSON.stringify(checkpoint, null, 2)}\n` },
117
+ ], { policyMode: 'exact' });
118
+ await buildIndex(root);
119
+ return { forgotten: true, alreadyHistorical: false, item: updated, checkpoint: checkpointId, canonicalHash: sha256(renderKnowledge(updated)) };
120
+ });
121
+ }
122
+ //# sourceMappingURL=memory-operations.js.map