@prodero/curator 0.0.0-stage → 0.2.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 (148) hide show
  1. package/ARCHITECTURE.md +298 -0
  2. package/CHANGELOG.md +33 -0
  3. package/CONTRIBUTING.md +9 -0
  4. package/LICENSE +21 -0
  5. package/README.md +93 -2
  6. package/SECURITY.md +7 -0
  7. package/bin/prodero +7 -0
  8. package/dist/cli.js +266 -0
  9. package/dist/context-instructions.js +2 -0
  10. package/dist/contracts.js +51 -0
  11. package/dist/conversation-presentation.js +297 -0
  12. package/dist/core/agent-instructions.js +52 -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 +117 -0
  24. package/dist/core/curation-question.js +28 -0
  25. package/dist/core/curation-review-presentation.js +4 -0
  26. package/dist/core/curation-review-state.js +62 -0
  27. package/dist/core/curation-review.js +573 -0
  28. package/dist/core/curation-revision.js +106 -0
  29. package/dist/core/curation-semantic-contract.js +242 -0
  30. package/dist/core/curation-semantic-jobs.js +340 -0
  31. package/dist/core/diagnostics.js +17 -0
  32. package/dist/core/format.js +114 -0
  33. package/dist/core/ids.js +6 -0
  34. package/dist/core/imports.js +169 -0
  35. package/dist/core/index.js +93 -0
  36. package/dist/core/instruction-review.js +86 -0
  37. package/dist/core/integrity-files.js +174 -0
  38. package/dist/core/intelligence.js +95 -0
  39. package/dist/core/knowledge-analysis.js +46 -0
  40. package/dist/core/knowledge-authority.js +143 -0
  41. package/dist/core/knowledge-curation.js +316 -0
  42. package/dist/core/knowledge-engine.js +475 -0
  43. package/dist/core/knowledge-graph.js +192 -0
  44. package/dist/core/knowledge-lifecycle.js +212 -0
  45. package/dist/core/knowledge-management.js +113 -0
  46. package/dist/core/knowledge-migration.js +84 -0
  47. package/dist/core/knowledge-policy.js +38 -0
  48. package/dist/core/knowledge-provenance.js +90 -0
  49. package/dist/core/knowledge-write-policy.js +85 -0
  50. package/dist/core/language.js +14 -0
  51. package/dist/core/local-preferences.js +76 -0
  52. package/dist/core/matching.js +6 -0
  53. package/dist/core/memory-index.js +241 -0
  54. package/dist/core/memory-intelligence.js +288 -0
  55. package/dist/core/memory-link-repair.js +70 -0
  56. package/dist/core/memory-operations.js +122 -0
  57. package/dist/core/memory-pressure.js +212 -0
  58. package/dist/core/memory-sync-curator.js +82 -0
  59. package/dist/core/memory-sync.js +558 -0
  60. package/dist/core/memory-transformation-bundle.js +163 -0
  61. package/dist/core/memory-transformation-proposal.js +135 -0
  62. package/dist/core/memory-transformation.js +108 -0
  63. package/dist/core/memory.js +186 -0
  64. package/dist/core/migrations.js +569 -0
  65. package/dist/core/npm-update.js +289 -0
  66. package/dist/core/path-scope.js +27 -0
  67. package/dist/core/plugin-files.js +116 -0
  68. package/dist/core/project-root.js +39 -0
  69. package/dist/core/project.js +634 -0
  70. package/dist/core/promotion.js +279 -0
  71. package/dist/core/reconciliation.js +61 -0
  72. package/dist/core/secrets.js +67 -0
  73. package/dist/core/semantic-safety.js +33 -0
  74. package/dist/core/session-index.js +116 -0
  75. package/dist/core/session-review.js +150 -0
  76. package/dist/core/sessions.js +289 -0
  77. package/dist/core/settings.js +94 -0
  78. package/dist/core/skill-curation.js +239 -0
  79. package/dist/core/skill-polish.js +41 -0
  80. package/dist/core/skills.js +387 -0
  81. package/dist/core/storage.js +462 -0
  82. package/dist/core/sync-transaction.js +290 -0
  83. package/dist/core/telemetry.js +64 -0
  84. package/dist/core/templates.js +53 -0
  85. package/dist/core/types.js +2 -0
  86. package/dist/core/update.js +326 -0
  87. package/dist/curation-review-adapter.js +92 -0
  88. package/dist/diagnostic-boundary.js +55 -0
  89. package/dist/filesystem/permissions.js +39 -0
  90. package/dist/hosts/adapters.js +119 -0
  91. package/dist/hosts/files.js +133 -0
  92. package/dist/hosts/manager.js +150 -0
  93. package/dist/hosts/public-entry.js +47 -0
  94. package/dist/hosts/runtime-command.js +66 -0
  95. package/dist/hosts/runtime.js +139 -0
  96. package/dist/hosts/wizard.js +114 -0
  97. package/dist/knowledge-table.js +38 -0
  98. package/dist/mcp-development.js +69 -0
  99. package/dist/mcp-workspace.js +141 -0
  100. package/dist/memory-presentation.js +182 -0
  101. package/dist/memory-sync-operation.js +88 -0
  102. package/dist/plugins/api.js +2 -0
  103. package/dist/plugins/context.js +20 -0
  104. package/dist/plugins/mcp.js +72 -0
  105. package/dist/plugins/registry.js +106 -0
  106. package/dist/presentation-cards.js +32 -0
  107. package/dist/presentation-server.js +94 -0
  108. package/dist/presentation-trace.js +174 -0
  109. package/dist/presentation.js +443 -0
  110. package/dist/providers/mcp-sampling.js +45 -0
  111. package/dist/server.js +664 -0
  112. package/dist/settings-definition.js +22 -0
  113. package/dist/skill-instructions.js +2 -0
  114. package/dist/status-presentation.js +20 -0
  115. package/dist/text-layout.js +278 -0
  116. package/dist/update-command.js +53 -0
  117. package/dist/update-instructions.js +4 -0
  118. package/dist-ui/index.html +216 -0
  119. package/docs/APPLICABILITY.md +151 -0
  120. package/docs/COMPATIBILITY.md +68 -0
  121. package/docs/CONTEXT-SELECTION-V2.md +61 -0
  122. package/docs/CURATION-APP-REVIEW.md +156 -0
  123. package/docs/CURATION-CLAUSE-REFINEMENT.md +45 -0
  124. package/docs/IMPORTER-CURATION.md +142 -0
  125. package/docs/KNOWLEDGE-AUTHORITY.md +165 -0
  126. package/docs/KNOWLEDGE-SCHEMA-V2.md +150 -0
  127. package/docs/MIGRATIONS.md +103 -0
  128. package/docs/PRIVACY.md +28 -0
  129. package/docs/RECOVERY.md +58 -0
  130. package/docs/RELEASING.md +98 -0
  131. package/docs/hosts.md +152 -0
  132. package/docs/images/prodero-context-ready-cropped.png +0 -0
  133. package/docs/images/prodero-overview-cropped.png +0 -0
  134. package/docs/images/prodero-session-learned-cropped.png +0 -0
  135. package/docs/plugins.md +83 -0
  136. package/docs/ui-themes.md +49 -0
  137. package/instructions/AGENTS.md +39 -0
  138. package/package.json +92 -4
  139. package/plugins/migrate_ai_studio/README.md +101 -0
  140. package/plugins/migrate_ai_studio/index.cjs +396 -0
  141. package/plugins/migrate_ai_studio/instructions/audit.md +25 -0
  142. package/plugins/migrate_ai_studio/instructions/classify.md +41 -0
  143. package/plugins/migrate_ai_studio/instructions/migrate.md +34 -0
  144. package/plugins/migrate_ai_studio/instructions/verify.md +20 -0
  145. package/plugins/migrate_ai_studio/plugin.json +11 -0
  146. package/plugins/migrate_ai_studio/schema.cjs +20 -0
  147. package/plugins/migrate_ai_studio/ui/dist/index.html +77 -0
  148. package/prodero.release.json +5 -0
@@ -0,0 +1,558 @@
1
+ import { loadKnowledgeGraph } from './knowledge-graph.js';
2
+ import { memorySyncOperation } from '../memory-sync-operation.js';
3
+ import { curationAnswers } from './curation-semantic-jobs.js';
4
+ import { graphicalReviewPayload } from './curation-graphical.js';
5
+ import { previewMemoryLinkRepair, applyMemoryLinkRepair } from './memory-link-repair.js';
6
+ import { loadCurationReview, CurationReviewError } from './curation-review.js';
7
+ import { requireCurrentProject } from "./migrations.js";
8
+ import { publicDiagnostic } from './diagnostics.js';
9
+ import { prepareSyncCuration, inspectSyncCuration, previewSyncCuration, refreshSyncQuestions } from './memory-sync-curator.js';
10
+ /** User-initiated orchestration. One bounded local workflow; no provider or background caller. */
11
+ import fs from 'node:fs/promises';
12
+ import { randomUUID } from 'node:crypto';
13
+ import { z } from 'zod';
14
+ import { checkedDataPath, ensureDataDirectory, writeFileAtomic, sha256, listCheckpoints, readConfig } from './storage.js';
15
+ import { withSyncLock, recoverSyncTransaction, fileHash } from './sync-transaction.js';
16
+ import { readKnowledgeInventory } from './knowledge-provenance.js';
17
+ import { getMemoryIntelligencePlan } from './memory-intelligence.js';
18
+ import { prepareMemoryTransformation, validateMemoryTransformationProposal, memoryTransformationProposalSchema } from './memory-transformation-proposal.js';
19
+ import { previewMemoryTransformation, applyMemoryTransformation } from './memory-transformation.js';
20
+ import { promotionRecommendations, previewKnowledgePromotion, applyKnowledgePromotion } from './promotion.js';
21
+ import { discoverSessions } from './session-index.js';
22
+ import { syncProject, autoMigrateProject, findProjectRoot } from './project.js';
23
+ const FILE = 'local/memory-sync.json';
24
+ export const MEMORY_SYNC_LIMITS = { workflows: 1, candidates: 20, promotions: 50, bytes: 4 * 1024 * 1024 };
25
+ const digest = (v) => sha256(JSON.stringify(v));
26
+ const id = z.string().regex(/^area_[a-f0-9]{20}$/);
27
+ const fingerprintSchema = z.object({ knowledge: z.string(), telemetry: z.string(), checkpoints: z.string() }).strict();
28
+ const jobSchema = z.object({ id, label: z.string(), signals: z.array(z.string()), recordCount: z.number(), counts: z.record(z.number()).optional(), categories: z.record(z.number()).optional(), sourceIds: z.array(z.string()).max(24), layers: z.object({ local: z.number(), versioned: z.number() }), selected: z.boolean(), status: z.enum(['unselected', 'pending', 'semantic_work', 'awaiting_review', 'awaiting_clarification', 'deferred', 'applied', 'skipped', 'failed']), curationReviewId: z.string().optional(), curationStateHash: z.string().optional(), error: z.string().optional(), unresolved: z.number().optional(), result: z.unknown().optional() }).strict();
29
+ const stateSchema = z.object({ version: z.literal(1), id: z.string().uuid(), root: z.string(), revision: z.number().int(), createdAt: z.string(), state: z.enum(['awaiting_selection', 'semantic_work', 'awaiting_clarification', 'awaiting_review', 'applying', 'awaiting_promotion_selection', 'completed', 'stale']), improveEnabled: z.boolean().optional(), repairLinksEnabled: z.boolean().optional(), brokenLinks: z.number().int().nonnegative().optional(), repairLinksDone: z.boolean().optional(), repairLinksResult: z.object({ fixed: z.number(), remaining: z.number() }).optional(),
30
+ usage: z.object({ sessions: z.number(), available: z.boolean() }).optional(), baseline: fingerprintSchema, jobs: z.array(jobSchema).max(20), promotionEnabled: z.boolean(), promotionDone: z.boolean(), promotionCandidates: z.array(z.object({ id: z.string(), title: z.string(), score: z.number(), signals: z.array(z.string()), recommended: z.boolean() })).max(50), promotionTotal: z.number(), promotionResult: z.unknown().optional(), recovery: z.object({ sessions: z.number(), created: z.number() }), error: z.string().optional(),
31
+ active: z.object({ kind: z.enum(['transformation', 'promotion', 'link-repair']), reviewId: z.string(), candidateId: id.optional(), preview: z.unknown(), approved: z.boolean(), unaffectedKnowledge: z.string().optional(), touchedIds: z.array(z.string()).max(150).optional() }).strict().optional(),
32
+ }).strict();
33
+ const requestIdentity = { workflowId: z.string().uuid(), revision: z.number().int().min(1) };
34
+ export const memorySyncActionSchema = z.discriminatedUnion('action', [
35
+ z.object({ action: z.literal('start'), restart: z.boolean().optional() }).strict(),
36
+ z.object({ action: z.literal('inspect'), workflowId: z.string().uuid().optional() }).strict(),
37
+ z.object({ action: z.literal('select'), ...requestIdentity, improve: z.boolean().optional(), repairLinks: z.boolean().optional(), share: z.boolean().optional(), candidateIds: z.array(id).max(20).optional(), reviewPromotion: z.boolean().optional() }).strict(),
38
+ z.object({ action: z.literal('prepare'), ...requestIdentity, candidateId: id }).strict(),
39
+ z.object({ action: z.literal('propose'), ...requestIdentity, candidateId: id, proposal: memoryTransformationProposalSchema }).strict(),
40
+ z.object({ action: z.literal('approve'), ...requestIdentity, reviewId: z.string(), approved: z.literal(true) }).strict(),
41
+ z.object({ action: z.literal('skip'), ...requestIdentity, candidateId: id.optional() }).strict(),
42
+ z.object({ action: z.literal('retry'), ...requestIdentity, candidateId: id }).strict(),
43
+ z.object({ action: z.literal('refresh_questions'), ...requestIdentity, candidateId: id, reviewId: z.string(), reviewRevisionId: z.string(), planHash: z.string(), decisionRevision: z.number().int().nonnegative(), answerLogHash: z.string() }).strict(),
44
+ z.object({ action: z.literal('promotions'), ...requestIdentity, ids: z.array(z.string()).max(50) }).strict(),
45
+ z.object({ action: z.literal('complete'), ...requestIdentity }).strict(),
46
+ ]);
47
+ async function knowledgeHash(root, exclude = []) { return digest((await readKnowledgeInventory(root)).filter(r => !exclude.includes(r.item.id)).map(r => [r.item.id, r.path, r.revision])); }
48
+ async function fingerprint(root, excludeCheckpoint) { return { knowledge: await knowledgeHash(root), telemetry: await fileHash(await checkedDataPath(root, 'local/memory-pressure.json')) ?? 'missing', checkpoints: digest((await listCheckpoints(root)).filter(c => c.id !== excludeCheckpoint).map(c => [c.id, digest(c)])) }; }
49
+ async function load(root) { const target = await checkedDataPath(root, FILE); try {
50
+ const stat = await fs.stat(target);
51
+ if (stat.size > MEMORY_SYNC_LIMITS.bytes)
52
+ throw new Error('Curate Memory state exceeds bound');
53
+ const envelope = JSON.parse(await fs.readFile(target, 'utf8'));
54
+ if (envelope.hash !== digest(envelope.workflow))
55
+ throw new Error('Curate Memory checkpoint is corrupt');
56
+ const state = stateSchema.parse(envelope.workflow);
57
+ if (state.root !== await fs.realpath(root))
58
+ throw new Error('Curate Memory belongs to another project');
59
+ return state;
60
+ }
61
+ catch (e) {
62
+ if (e.code === 'ENOENT')
63
+ return null;
64
+ throw e;
65
+ } }
66
+ async function save(root, w) { w.revision++; const workflow = stateSchema.parse(w); const text = JSON.stringify({ hash: digest(workflow), workflow }); if (Buffer.byteLength(text) > MEMORY_SYNC_LIMITS.bytes)
67
+ throw new Error('Curate Memory checkpoint exceeds bounded storage; split work'); await ensureDataDirectory(root, 'local'); await writeFileAtomic(await checkedDataPath(root, FILE), text); }
68
+ async function promotions(root, w) { const rows = await promotionRecommendations(root, { persistCache: false }); w.promotionCandidates = rows.slice(0, 50); w.promotionTotal = rows.length; }
69
+ function currentJob(w) { return w.jobs.find(j => j.selected && ['pending', 'semantic_work', 'awaiting_review', 'awaiting_clarification'].includes(j.status)); }
70
+ async function advance(root, w) {
71
+ if (w.repairLinksEnabled && !w.repairLinksDone) {
72
+ const preview = await previewMemoryLinkRepair(root);
73
+ if (preview.count) {
74
+ w.active = { kind: 'link-repair', reviewId: preview.previewId, preview, approved: false };
75
+ w.state = 'awaiting_review';
76
+ return;
77
+ }
78
+ w.repairLinksDone = true;
79
+ }
80
+ if (currentJob(w))
81
+ w.state = 'semantic_work';
82
+ else if (w.promotionEnabled && !w.promotionDone) {
83
+ await promotions(root, w);
84
+ if (w.improveEnabled !== undefined && !w.promotionCandidates.length) {
85
+ w.promotionDone = true;
86
+ w.state = 'completed';
87
+ }
88
+ else
89
+ w.state = 'awaiting_promotion_selection';
90
+ }
91
+ else
92
+ w.state = 'completed';
93
+ }
94
+ async function deferredQuestions(root, w) {
95
+ const questions = [];
96
+ for (const job of w.jobs.filter(j => j.selected && j.status === 'deferred' && j.curationReviewId)) {
97
+ const payload = await inspectSyncCuration(root, job.curationReviewId), review = payload.review, g = payload.graphical;
98
+ const seen = new Set();
99
+ for (const q of g.questions.filter(q => q.state === 'deferred')) {
100
+ const related = q.reopenedQuestions, key = related.map(other => other.id).sort().join(',');
101
+ if (seen.has(key))
102
+ continue;
103
+ seen.add(key);
104
+ questions.push({ candidateId: job.id, title: q.question.title, prompt: q.question.prompt, expired: payload.projection.lifecycle === 'stale', affectedQuestions: related,
105
+ action: 'question_reopen', reviewId: review.snapshot.reviewId, reviewRevisionId: review.snapshot.revision.id, planHash: review.snapshot.planHash, decisionRevision: review.state.revision, answerLogHash: g.answerLogHash, questionId: q.question.id, questionVersion: q.question.version });
106
+ }
107
+ }
108
+ return questions;
109
+ }
110
+ /** Adopt only an explicitly reopened saved question. Inspection never reopens
111
+ * one, preempts independent reasoning, or replaces an active exact review. */
112
+ async function resumeRevisitedQuestion(root, w) {
113
+ if (!w.improveEnabled || currentJob(w) || w.active || ['stale', 'applying', 'awaiting_selection'].includes(w.state))
114
+ return;
115
+ for (const job of w.jobs.filter(j => j.selected && j.status === 'deferred' && j.curationReviewId)) {
116
+ const payload = await inspectSyncCuration(root, job.curationReviewId);
117
+ if (!payload.graphical.activeQuestionCount && !payload.graphical.analysisPending && !payload.graphical.questions.some(q => q.state === 'answered_pending_analysis'))
118
+ continue;
119
+ job.curationReviewId = payload.review.snapshot.reviewId;
120
+ job.status = 'semantic_work';
121
+ delete job.error;
122
+ w.state = 'semantic_work';
123
+ await save(root, w);
124
+ return;
125
+ }
126
+ }
127
+ function view(w, clarification, deferred = []) {
128
+ const applied = w.jobs.filter(j => j.status === 'applied').map(j => j.result.summary);
129
+ const promotion = w.promotionResult;
130
+ return { ...w, clarification, deferredQuestions: deferred, active: undefined, review: w.active ? { kind: w.active.kind, reviewId: w.active.reviewId, preview: w.active.preview } : undefined,
131
+ health: { transformationCandidates: w.jobs.length, contextPressureCandidates: w.jobs.filter(j => j.signals.includes('context_pressure')).length, potentialConflictCandidates: w.jobs.filter(j => j.signals.includes('potential_conflict')).length, localPromotionCandidates: w.promotionTotal, possibleVersionedTransformationRecords: w.jobs.reduce((n, j) => n + j.layers.versioned, 0) },
132
+ summary: { brokenLinksFixed: w.repairLinksResult?.fixed ?? 0, brokenLinksRemaining: w.repairLinksResult?.remaining ?? w.brokenLinks ?? 0, areasProcessed: applied.length, areasFailed: w.jobs.filter(j => j.status === 'failed').length, areasSkipped: w.jobs.filter(j => j.status === 'skipped').length, questionsDeferred: w.jobs.filter(j => j.status === 'deferred').length,
133
+ sourcesBefore: applied.reduce((n, r) => n + r.consolidated.length + r.superseded.length + r.kept.length + r.unresolved.length, 0), currentAfter: applied.reduce((n, r) => n + r.created.length + r.kept.length + r.unresolved.length, 0),
134
+ archived: applied.reduce((n, r) => n + r.archives.length, 0), superseded: applied.reduce((n, r) => n + r.superseded.length, 0), unresolved: w.jobs.reduce((n, j) => n + (j.unresolved ?? 0), 0),
135
+ estimatedTokens: { scope: 'transformed_areas', before: applied.reduce((n, r) => n + r.estimatedTokens.before, 0), after: applied.reduce((n, r) => n + r.estimatedTokens.after, 0) },
136
+ versionedCreated: applied.reduce((n, r) => n + r.created.filter(c => c.layer === 'versioned').length, 0), promoted: promotion?.published ?? 0, promotionCreated: promotion?.created.length ?? 0, promotionUpdated: promotion?.updated.length ?? 0, promotionCovered: promotion?.covered.length ?? 0, localArchived: applied.reduce((n, r) => n + r.archives.filter(a => a.layer === 'local').length, 0), versionedArchived: applied.reduce((n, r) => n + r.archives.filter(a => a.layer === 'versioned').length, 0), acknowledgedCandidateIds: w.jobs.filter(j => j.status === 'applied').map(j => j.id), promotionConflicts: promotion?.conflicts.length ?? 0, historicalProvenancePreserved: true },
137
+ coverage: { promotionCandidatesOmitted: Math.max(0, w.promotionTotal - w.promotionCandidates.length) },
138
+ next: w.state === 'semantic_work' ? { action: 'semantic_work_required', candidateId: currentJob(w)?.id, instructions: w.improveEnabled ? 'Use prodero_memory_sync prepare for the next candidate. For its curation job, use the existing prodero_curation_reasoning_prepare/propose protocol, then inspect this Sync workflow. Continue sequentially until a question, review or completion. Never Apply or approve; Core handles eligible Local changes. No session lifecycle or extra scans.' : 'Use prodero_memory_sync action=prepare, then reason only over that one bundle and submit action=propose with the returned workflow ID/revision. Continue sequentially while semantic_work_required. Never approve review automatically. Do not use session prepare/record/close for this workflow.' } : ['awaiting_clarification', 'awaiting_promotion_selection', 'awaiting_review'].includes(w.state) ? { action: w.state === 'awaiting_review' ? 'human_review_required' : w.state, tool: 'prodero_sync', arguments: { projectRoot: w.root, workflowId: w.id }, instructions: 'When an unattached continuation returns this state, present the pending human step before ending the turn: read project presentation settings and invoke prodero_sync with these exact arguments once (use prodero_text for text mode). This reopens the same saved workflow without restarting analysis. After presenting it, stop and wait; do not repeatedly reopen an unchanged step. Never select records, answer questions, approve or publish for the human.' } : { action: w.state },
139
+ };
140
+ }
141
+ async function workflowView(root, w, clarification, deferred) {
142
+ const previous = currentJob(w)?.id;
143
+ if (w.state !== 'stale')
144
+ await resumeRevisitedQuestion(root, w);
145
+ if (!previous && currentJob(w))
146
+ clarification = await refreshCuration(root, w);
147
+ const snapshot = view(w, clarification, deferred ?? (w.state !== 'stale' ? await deferredQuestions(root, w) : []));
148
+ const lineages = new Set(), questions = new Set();
149
+ let savedAnswerEvents = 0, unsupersededAnswerEvents = 0;
150
+ let reasoning;
151
+ for (const job of w.jobs.filter(j => j.selected && j.curationReviewId)) {
152
+ const payload = clarification && clarification.review?.snapshot.reviewId === job.curationReviewId ? clarification : await graphicalReviewPayload(root, await loadCurationReview(root, job.curationReviewId, { allowExpired: true }));
153
+ const lineage = payload.review.snapshot.revision.lineageId;
154
+ if (job.id === currentJob(w)?.id)
155
+ reasoning = payload.graphical;
156
+ if (lineages.has(lineage))
157
+ continue;
158
+ lineages.add(lineage);
159
+ for (const q of payload.graphical.questions)
160
+ if (q.state === 'deferred')
161
+ questions.add(`${lineage}:${q.question.id}`);
162
+ const answers = await curationAnswers(root, lineage);
163
+ savedAnswerEvents += answers.length;
164
+ unsupersededAnswerEvents += answers.filter(a => !answers.some(next => next.replacesAnswerId === a.id)).length;
165
+ }
166
+ const projected = { ...snapshot, reasoning, questionCounts: { deferred: questions.size, savedAnswerEvents, unsupersededAnswerEvents, coverage: 'saved_selected_reviews' } };
167
+ return { ...projected, operation: memorySyncOperation(projected) };
168
+ }
169
+ async function refreshCuration(root, w) {
170
+ const j = currentJob(w);
171
+ if (!w.improveEnabled || !j?.curationReviewId || !['semantic_work', 'awaiting_clarification'].includes(w.state))
172
+ return;
173
+ const before = digest(w);
174
+ let payload;
175
+ try {
176
+ payload = await inspectSyncCuration(root, j.curationReviewId);
177
+ }
178
+ catch (e) {
179
+ j.status = 'failed';
180
+ j.error = publicDiagnostic(e).slice(0, 1000);
181
+ await advance(root, w);
182
+ await save(root, w);
183
+ return;
184
+ }
185
+ const g = payload.graphical;
186
+ j.curationReviewId = payload.review.snapshot.reviewId;
187
+ j.curationStateHash = digest([j.curationReviewId, payload.review.state.revision, g.answerLogHash, g.jobs]);
188
+ j.unresolved = payload.projection.unresolvedIssueCount;
189
+ if (g.analysisPending) {
190
+ const answering = g.questions.some(q => q.state === 'answered_pending_analysis');
191
+ w.state = answering ? 'awaiting_clarification' : 'semantic_work';
192
+ j.status = w.state;
193
+ if (digest(w) !== before)
194
+ await save(root, w);
195
+ return answering ? payload : undefined;
196
+ }
197
+ if (g.activeQuestionCount || g.analysisFailed && g.questions.some(q => q.state === 'answered_pending_analysis')) {
198
+ w.state = 'awaiting_clarification';
199
+ j.status = 'awaiting_clarification';
200
+ j.unresolved = payload.projection.unresolvedIssueCount;
201
+ if (digest(w) !== before)
202
+ await save(root, w);
203
+ return payload;
204
+ }
205
+ if (g.deferredQuestionIds.length || payload.projection.paused) {
206
+ j.status = 'deferred';
207
+ j.unresolved = payload.projection.unresolvedIssueCount;
208
+ await advance(root, w);
209
+ await save(root, w);
210
+ return;
211
+ }
212
+ if (g.analysisFailed) {
213
+ j.status = 'failed';
214
+ j.error = 'Analysis could not finish; saved answers are retained.';
215
+ await advance(root, w);
216
+ await save(root, w);
217
+ return;
218
+ }
219
+ const typed = payload.review.snapshot.revision.typedPlan;
220
+ if (typed.kind !== 'semantic-v2')
221
+ return payload;
222
+ let preview;
223
+ try {
224
+ preview = await previewSyncCuration(root, j.id, payload);
225
+ }
226
+ catch (e) {
227
+ j.status = 'failed';
228
+ j.error = publicDiagnostic(e).slice(0, 1000);
229
+ await advance(root, w);
230
+ await save(root, w);
231
+ return;
232
+ }
233
+ if (!preview) {
234
+ j.status = 'skipped';
235
+ j.unresolved = payload.projection.unresolvedIssueCount;
236
+ j.error = 'Knowledge retained because the available evidence did not support a safe change.';
237
+ await advance(root, w);
238
+ await save(root, w);
239
+ return;
240
+ }
241
+ if (!preview.localDirectApplyEligible)
242
+ throw new Error('Curator Sync result is not eligible for automatic Local apply');
243
+ w.active = { kind: 'transformation', candidateId: j.id, reviewId: preview.previewId, preview, approved: false };
244
+ await beginApply(root, w);
245
+ }
246
+ async function finishApply(root, w) {
247
+ const a = w.active;
248
+ if ((a.kind === 'promotion' || a.kind === 'link-repair') && !a.approved)
249
+ throw new Error('This operation requires persisted exact workflow approval');
250
+ let result;
251
+ if (a.kind === 'transformation') {
252
+ const p = a.preview;
253
+ result = await applyMemoryTransformation(root, p, { initiatedByUser: true, reviewedPreviewId: p.previewId, approvedVersioned: a.approved, approvedSemanticLimitations: a.approved });
254
+ w.jobs.find(j => j.id === a.candidateId).status = 'applied';
255
+ w.jobs.find(j => j.id === a.candidateId).result = result;
256
+ }
257
+ else if (a.kind === 'link-repair') {
258
+ result = await applyMemoryLinkRepair(root, a.preview, a.approved);
259
+ w.repairLinksResult = { fixed: result.fixed, remaining: result.remaining };
260
+ w.repairLinksDone = true;
261
+ }
262
+ else {
263
+ result = await applyKnowledgePromotion(root, a.preview, true);
264
+ w.promotionResult = result;
265
+ w.promotionDone = true;
266
+ }
267
+ // A crash after commit must not silently adopt unrelated external changes on resume.
268
+ const now = await fingerprint(root, result.checkpoint);
269
+ const revisions = a.kind === 'transformation' ? a.preview.lifecycle.changes.map(c => ({ path: c.to, hash: sha256(c.after) })) : result.revisions;
270
+ const unchanged = now.telemetry === w.baseline.telemetry && now.checkpoints === w.baseline.checkpoints && await knowledgeHash(root, a.touchedIds) === a.unaffectedKnowledge;
271
+ const exact = (await Promise.all(revisions.map(async (r) => await fileHash(await checkedDataPath(root, r.path)) === r.hash))).every(Boolean);
272
+ delete w.active;
273
+ if (!unchanged || !exact) {
274
+ w.state = 'stale';
275
+ w.error = 'Operation committed, but project changed independently. Start a fresh workflow.';
276
+ }
277
+ else {
278
+ w.baseline = await fingerprint(root);
279
+ await advance(root, w);
280
+ }
281
+ await save(root, w);
282
+ }
283
+ async function beginApply(root, w) {
284
+ const a = w.active;
285
+ a.touchedIds = a.kind === 'link-repair' ? a.preview.changes.map(c => c.id) : a.kind === 'transformation' ? a.preview.lifecycle.changes.map(c => c.id) : a.preview.actions.flatMap(c => [...c.localIds, ...(c.targetId ? [c.targetId] : []), ...(c.draft ? [c.draft.id] : [])]);
286
+ a.unaffectedKnowledge = await knowledgeHash(root, a.touchedIds);
287
+ w.state = 'applying';
288
+ await save(root, w); // authorization/intention durable BEFORE knowledge journal
289
+ await finishApply(root, w);
290
+ }
291
+ async function resume(root, w) {
292
+ if (w.state === 'applying') {
293
+ try {
294
+ await finishApply(root, w);
295
+ }
296
+ catch (e) {
297
+ w.error = publicDiagnostic(e).slice(0, 1000);
298
+ if (/stale|changed|mismatch/i.test(w.error) && !await fileHash(await checkedDataPath(root, '.transactions/pending.json')))
299
+ w.state = 'stale';
300
+ await save(root, w);
301
+ }
302
+ return;
303
+ }
304
+ if (w.state === 'stale')
305
+ return;
306
+ if (digest(await fingerprint(root)) !== digest(w.baseline)) {
307
+ w.state = 'stale';
308
+ w.error = 'Knowledge, telemetry or checkpoints changed. Start a fresh workflow; old approvals cannot be reused.';
309
+ await save(root, w);
310
+ }
311
+ }
312
+ export async function runMemorySync(root, raw) {
313
+ const input = memorySyncActionSchema.parse(raw);
314
+ root = await findProjectRoot(root);
315
+ if (!await readConfig(root))
316
+ throw new Error('Prodero must be initialized before Curate Memory');
317
+ if (input.action === 'start') {
318
+ await requireCurrentProject(root);
319
+ await autoMigrateProject(root);
320
+ // Explicit start recovers observations only when pending; no semantic maintenance on open.
321
+ const existing = await load(root);
322
+ if (existing && !('restart' in input && input.restart) && !['completed', 'stale'].includes(existing.state))
323
+ return runMemorySync(root, { action: 'inspect', workflowId: existing.id });
324
+ let recovery = { sessions: 0, created: 0 };
325
+ await withSyncLock(root, () => recoverSyncTransaction(root));
326
+ const discovery = await discoverSessions(root, { loadPendingBodies: false });
327
+ if (discovery.sessions.some(s => s.pending)) {
328
+ const synced = await syncProject(root);
329
+ recovery = { sessions: synced.sessions, created: synced.created };
330
+ }
331
+ return withSyncLock(root, async () => {
332
+ const plan = await getMemoryIntelligencePlan(root);
333
+ const w = { brokenLinks: (await loadKnowledgeGraph(root, undefined, { persistCache: false })).diagnostics.brokenReferences, version: 1, id: randomUUID(), root: await fs.realpath(root), revision: 0, createdAt: new Date().toISOString(), state: 'awaiting_selection', usage: { sessions: plan.coverage.sessions, available: plan.coverage.telemetry === 'available' }, baseline: await fingerprint(root), jobs: plan.candidates.map(c => ({ id: c.id, label: c.label, signals: c.signals, recordCount: c.recordCount, counts: c.counts, categories: c.categories, sourceIds: c.records.map(r => r.id), layers: c.layers, selected: false, status: 'unselected' })), promotionEnabled: false, promotionDone: false, promotionCandidates: [], promotionTotal: 0, recovery };
334
+ await promotions(root, w);
335
+ await save(root, w);
336
+ return workflowView(root, w);
337
+ });
338
+ }
339
+ return withSyncLock(root, async () => {
340
+ await recoverSyncTransaction(root);
341
+ const w = await load(root);
342
+ if (!w)
343
+ throw new Error('Start Curate Memory first');
344
+ if (input.workflowId && input.workflowId !== w.id)
345
+ throw new Error('Curate Memory workflow expired or was replaced');
346
+ const before = w.revision;
347
+ await resume(root, w);
348
+ if (input.action === 'inspect') {
349
+ if (w.state !== 'stale')
350
+ await resumeRevisitedQuestion(root, w);
351
+ const clarification = w.state !== 'stale' ? await refreshCuration(root, w) : undefined;
352
+ // Finishing independent work may make a deliberately reopened question next.
353
+ if (w.state !== 'stale')
354
+ await resumeRevisitedQuestion(root, w);
355
+ return workflowView(root, w, clarification ?? (w.state !== 'stale' ? await refreshCuration(root, w) : undefined), w.state !== 'stale' ? await deferredQuestions(root, w) : []);
356
+ }
357
+ if (w.state === 'stale')
358
+ throw new Error(w.error);
359
+ if (w.state === 'applying')
360
+ throw new Error('Approved apply is pending recovery; inspect/resume before another action');
361
+ if (!('revision' in input) || input.revision !== before || w.revision !== before)
362
+ throw new Error('Stale workflow revision; inspect current workflow');
363
+ delete w.error;
364
+ if (input.action === 'select') {
365
+ if (w.state !== 'awaiting_selection')
366
+ throw new Error('Selection is already fixed');
367
+ const modern = input.candidateIds === undefined && input.reviewPromotion === undefined;
368
+ if (!modern && (input.improve !== undefined || input.share !== undefined || input.repairLinks !== undefined))
369
+ throw new Error('Choose operations or legacy candidates, not both');
370
+ const improve = input.improve ?? true, share = input.share ?? true, repairLinks = Boolean(w.brokenLinks) && (input.repairLinks ?? true);
371
+ if (modern && !improve && !share && !repairLinks)
372
+ throw new Error('Select Improve memory, Fix broken links or Share knowledge');
373
+ const ids = modern ? (improve ? w.jobs.map(j => j.id) : []) : input.candidateIds ?? [];
374
+ if (new Set(ids).size !== ids.length || ids.some(id => !w.jobs.some(j => j.id === id)))
375
+ throw new Error('Unknown/duplicate candidate selection');
376
+ if (modern) {
377
+ w.improveEnabled = improve;
378
+ w.repairLinksEnabled = repairLinks;
379
+ }
380
+ for (const j of w.jobs) {
381
+ j.selected = ids.includes(j.id);
382
+ j.status = j.selected ? 'pending' : 'unselected';
383
+ }
384
+ w.promotionEnabled = modern ? share : input.reviewPromotion === true;
385
+ await advance(root, w);
386
+ }
387
+ else if (input.action === 'prepare') {
388
+ const j = currentJob(w);
389
+ if (w.state !== 'semantic_work' || !j || input.candidateId !== j.id)
390
+ throw new Error('Prepare only the next selected candidate');
391
+ if (w.improveEnabled) {
392
+ try {
393
+ const curation = await prepareSyncCuration(root, j.sourceIds, j.curationReviewId, j.status === 'pending' && !!j.curationReviewId);
394
+ j.curationReviewId = curation.reviewId;
395
+ j.status = 'semantic_work';
396
+ await save(root, w);
397
+ return { ...await workflowView(root, w), curation };
398
+ }
399
+ catch (e) {
400
+ j.status = 'failed';
401
+ j.error = publicDiagnostic(e).slice(0, 1000);
402
+ await advance(root, w);
403
+ await save(root, w);
404
+ return workflowView(root, w);
405
+ }
406
+ }
407
+ try {
408
+ const semantic = await prepareMemoryTransformation(root, j.id);
409
+ if (!semantic.canPropose)
410
+ throw new Error('Candidate requires splitting or complete evidence');
411
+ if (semantic.bundle.sourceIds.length !== j.recordCount || semantic.bundle.sourceIds.some(id => !j.sourceIds.includes(id)))
412
+ throw new Error('Candidate membership changed');
413
+ j.status = 'semantic_work';
414
+ await save(root, w);
415
+ return { ...await workflowView(root, w), semantic };
416
+ }
417
+ catch (e) {
418
+ j.status = 'failed';
419
+ j.error = publicDiagnostic(e).slice(0, 1000);
420
+ await advance(root, w);
421
+ }
422
+ }
423
+ else if (input.action === 'propose') {
424
+ if (w.improveEnabled)
425
+ throw new Error('Submit this job through Curator reasoning, then inspect Curate Memory');
426
+ const j = currentJob(w);
427
+ if (w.state !== 'semantic_work' || j?.status !== 'semantic_work' || input.candidateId !== j.id || !input.proposal)
428
+ throw new Error('Prepare selected candidate before submitting its proposal');
429
+ try {
430
+ if (input.proposal.candidateId !== j.id)
431
+ throw new Error('Proposal belongs to another candidate');
432
+ const validated = await validateMemoryTransformationProposal(root, input.proposal);
433
+ const preview = await previewMemoryTransformation(root, validated);
434
+ j.unresolved = preview.summary.unresolved.length;
435
+ w.active = { kind: 'transformation', candidateId: j.id, reviewId: preview.previewId, preview, approved: false };
436
+ j.status = 'awaiting_review';
437
+ w.state = 'awaiting_review';
438
+ await save(root, w);
439
+ }
440
+ catch (e) {
441
+ j.status = 'failed';
442
+ j.error = publicDiagnostic(e).slice(0, 1000);
443
+ await advance(root, w);
444
+ await save(root, w);
445
+ return workflowView(root, w);
446
+ }
447
+ if (w.active.preview.localDirectApplyEligible)
448
+ await beginApply(root, w);
449
+ return workflowView(root, w);
450
+ }
451
+ else if (input.action === 'approve') {
452
+ if (w.state !== 'awaiting_review' || !w.active || input.approved !== true || input.reviewId !== w.active.reviewId)
453
+ throw new Error('Explicit approval of this exact review is required');
454
+ w.active.approved = true;
455
+ await beginApply(root, w);
456
+ return workflowView(root, w);
457
+ }
458
+ else if (input.action === 'skip') {
459
+ if (w.active?.kind === 'link-repair') {
460
+ delete w.active;
461
+ w.repairLinksDone = true;
462
+ await advance(root, w);
463
+ }
464
+ else if (w.active?.kind === 'promotion') {
465
+ delete w.active;
466
+ w.promotionDone = true;
467
+ await advance(root, w);
468
+ }
469
+ else {
470
+ const j = currentJob(w);
471
+ if (!j || input.candidateId !== j.id)
472
+ throw new Error('Skip requires current candidate');
473
+ j.status = 'skipped';
474
+ delete w.active;
475
+ await advance(root, w);
476
+ }
477
+ }
478
+ else if (input.action === 'refresh_questions') {
479
+ const j = w.jobs.find(j => j.id === input.candidateId && j.selected && j.status === 'deferred' && j.curationReviewId);
480
+ if (!j || w.active)
481
+ throw new CurationReviewError('review_changed', 'Refresh only this saved deferred review after finishing the active exact review.');
482
+ if ((await inspectSyncCuration(root, j.curationReviewId)).review.snapshot.reviewId !== input.reviewId)
483
+ throw new CurationReviewError('review_changed', 'This deferred review changed. Inspect saved progress.');
484
+ const fresh = await refreshSyncQuestions(root, j.sourceIds, input);
485
+ j.curationReviewId = fresh.review.snapshot.reviewId;
486
+ }
487
+ else if (input.action === 'retry') {
488
+ if (w.active || w.promotionResult)
489
+ throw new Error('Finish review before retry, or start a fresh workflow after promotion');
490
+ const j = w.jobs.find(j => j.id === input.candidateId && j.selected && j.status === 'failed');
491
+ if (!j)
492
+ throw new Error('Only failed candidates can be retried');
493
+ j.status = 'pending';
494
+ delete j.error;
495
+ w.state = 'semantic_work';
496
+ }
497
+ else if (input.action === 'promotions') {
498
+ if (w.state !== 'awaiting_promotion_selection' || !w.promotionEnabled)
499
+ throw new Error('Finish selected transformations before promotion review');
500
+ const ids = input.ids ?? [];
501
+ if (!ids.length || new Set(ids).size !== ids.length || ids.some(id => !w.promotionCandidates.some(c => c.id === id)))
502
+ throw new Error('Select specific current Local promotion candidates');
503
+ const preview = await previewKnowledgePromotion(root, { ids });
504
+ w.active = { kind: 'promotion', reviewId: preview.previewId, preview, approved: false };
505
+ w.state = 'awaiting_review';
506
+ }
507
+ else if (input.action === 'complete') {
508
+ if (!['awaiting_promotion_selection', 'completed'].includes(w.state))
509
+ throw new Error('Finish or skip selected transformations first');
510
+ w.promotionDone = true;
511
+ w.state = 'completed';
512
+ }
513
+ await save(root, w);
514
+ return workflowView(root, w);
515
+ });
516
+ }
517
+ /** Open-only diagnostic projection. Never resumes, recovers, migrates or persists a workflow. */
518
+ export async function getMemorySyncOverview(root) {
519
+ const workflow = await load(root);
520
+ const pending = workflow && workflow.state !== 'completed' ? workflow : null;
521
+ const stale = pending && (pending.state === 'stale' || pending.state !== 'applying' && digest(await fingerprint(root)) !== digest(pending.baseline));
522
+ const workflowStatus = pending ? { id: pending.id, state: stale ? 'stale' : pending.state } : null;
523
+ try {
524
+ const plan = await getMemoryIntelligencePlan(root);
525
+ return { available: plan.coverage.telemetry !== 'unavailable', sessions: plan.coverage.sessions,
526
+ areas: plan.candidates.map(c => ({ id: c.id, label: c.label, recordCount: c.recordCount, signals: c.signals, counts: c.counts, layers: c.layers, categories: c.categories })),
527
+ areasOmitted: plan.truncated.candidates, workflow: workflowStatus };
528
+ }
529
+ catch {
530
+ return { available: false, sessions: 0, areas: [], areasOmitted: 0, workflow: workflowStatus };
531
+ }
532
+ }
533
+ /** Resolve continuation ownership from persisted review lineage, not a particular App instance. */
534
+ export async function syncWorkflowForCuration(root, lineageId) {
535
+ const w = await load(root);
536
+ if (!w || !w.improveEnabled || w.state === 'stale')
537
+ return undefined;
538
+ for (const job of w.jobs.filter(j => j.selected && j.curationReviewId && ['pending', 'semantic_work', 'awaiting_clarification', 'deferred'].includes(j.status))) {
539
+ const review = await loadCurationReview(root, job.curationReviewId, { allowExpired: true });
540
+ if (review.snapshot.revision?.lineageId === lineageId)
541
+ return w.id;
542
+ }
543
+ return undefined;
544
+ }
545
+ /** Internal adapter ownership check under the common writer lock. A review ID
546
+ * from another workflow/candidate never grants conversational access. */
547
+ export async function syncCurationReview(root, workflowId, candidateId, reviewId) {
548
+ const w = await load(root), job = w?.jobs.find(j => j.id === candidateId && j.selected && j.curationReviewId && ['pending', 'semantic_work', 'awaiting_clarification', 'deferred', 'failed'].includes(j.status));
549
+ if (!w || w.id !== workflowId || !w.improveEnabled || !job || w.state === 'stale')
550
+ throw new CurationReviewError('review_changed', 'This question is not part of the current Sync workflow. Inspect saved progress.');
551
+ if (digest(await fingerprint(root)) !== digest(w.baseline))
552
+ throw new CurationReviewError('semantic_stale', 'Knowledge or evidence changed. Inspect the current workflow.');
553
+ const payload = await inspectSyncCuration(root, job.curationReviewId);
554
+ if (payload.review.snapshot.reviewId !== reviewId)
555
+ throw new CurationReviewError('review_changed', 'This review changed. Inspect saved progress.');
556
+ return payload;
557
+ }
558
+ //# sourceMappingURL=memory-sync.js.map