@prodero/curator 0.1.0 → 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.
- package/ARCHITECTURE.md +1 -1
- package/CHANGELOG.md +19 -2
- package/README.md +32 -33
- package/dist/cli.js +58 -20
- package/dist/contracts.js +1 -0
- package/dist/conversation-presentation.js +62 -27
- package/dist/core/agent-instructions.js +31 -18
- package/dist/core/curation-graphical.js +22 -10
- package/dist/core/curation-question.js +28 -0
- package/dist/core/curation-semantic-contract.js +4 -1
- package/dist/core/curation-semantic-jobs.js +78 -18
- package/dist/core/knowledge-graph.js +5 -4
- package/dist/core/local-preferences.js +76 -0
- package/dist/core/memory-index.js +2 -0
- package/dist/core/memory-link-repair.js +6 -2
- package/dist/core/memory-sync-curator.js +25 -5
- package/dist/core/memory-sync.js +124 -29
- package/dist/core/npm-update.js +289 -0
- package/dist/core/project.js +8 -8
- package/dist/core/session-index.js +10 -6
- package/dist/core/sessions.js +3 -3
- package/dist/core/settings.js +34 -16
- package/dist/core/skills.js +21 -2
- package/dist/core/storage.js +55 -1
- package/dist/core/sync-transaction.js +85 -49
- package/dist/core/update.js +9 -3
- package/dist/curation-review-adapter.js +1 -1
- package/dist/diagnostic-boundary.js +4 -3
- package/dist/memory-presentation.js +26 -16
- package/dist/memory-sync-operation.js +88 -0
- package/dist/presentation-server.js +25 -10
- package/dist/presentation.js +92 -61
- package/dist/server.js +59 -51
- package/dist/settings-definition.js +3 -3
- package/dist/status-presentation.js +20 -0
- package/dist/text-layout.js +278 -0
- package/dist/update-command.js +53 -0
- package/dist/update-instructions.js +4 -0
- package/dist-ui/index.html +117 -115
- package/docs/COMPATIBILITY.md +29 -10
- package/docs/MIGRATIONS.md +4 -0
- package/docs/RECOVERY.md +3 -1
- package/docs/RELEASING.md +68 -10
- package/docs/hosts.md +25 -7
- package/docs/images/prodero-context-ready-cropped.png +0 -0
- package/docs/images/prodero-overview-cropped.png +0 -0
- package/docs/images/prodero-session-learned-cropped.png +0 -0
- package/docs/ui-themes.md +10 -4
- package/instructions/AGENTS.md +24 -18
- package/package.json +6 -2
|
@@ -1,19 +1,39 @@
|
|
|
1
1
|
/** Sync orchestration adapter. Questions, answers and jobs belong to Curator. */
|
|
2
|
-
import { createCurationReview, resumeCurationReview, latestCurationReview } from './curation-review.js';
|
|
2
|
+
import { createCurationReview, resumeCurationReview, latestCurationReview, loadCurationReview, assertCurationHead, CurationReviewError } from './curation-review.js';
|
|
3
3
|
import { planKnowledgeCuration } from './knowledge-curation.js';
|
|
4
4
|
import { graphicalReviewPayload } from './curation-graphical.js';
|
|
5
|
-
import { prepareSemanticCuration } from './curation-semantic-jobs.js';
|
|
5
|
+
import { prepareSemanticCuration, inspectSemanticCuration, curationAnswers } from './curation-semantic-jobs.js';
|
|
6
6
|
import { prepareMemoryTransformation, validateMemoryTransformationProposal } from './memory-transformation-proposal.js';
|
|
7
7
|
import { previewMemoryTransformation } from './memory-transformation.js';
|
|
8
|
-
import { scopeEffect } from './curation-semantic-contract.js';
|
|
8
|
+
import { scopeEffect, semanticHash } from './curation-semantic-contract.js';
|
|
9
|
+
import { deterministicBaseline } from './curation-revision.js';
|
|
10
|
+
/** Explicit expiry refresh carries staged semantics and non-approval decisions
|
|
11
|
+
* into a fresh revision. It neither reopens a question nor records an apply. */
|
|
12
|
+
export async function refreshSyncQuestions(root, sourceIds, input) {
|
|
13
|
+
const review = await loadCurationReview(root, input.reviewId, { allowExpired: true }), revision = review.snapshot.revision;
|
|
14
|
+
await assertCurationHead(root, review);
|
|
15
|
+
if (revision.id !== input.reviewRevisionId || review.snapshot.planHash !== input.planHash || review.state.revision !== input.decisionRevision || semanticHash(await curationAnswers(root, revision.lineageId)) !== input.answerLogHash)
|
|
16
|
+
throw new CurationReviewError('review_changed', 'This saved question changed. Inspect current progress.');
|
|
17
|
+
const payload = await graphicalReviewPayload(root, review), baseline = deterministicBaseline(revision);
|
|
18
|
+
if (payload.projection.lifecycle !== 'stale')
|
|
19
|
+
throw new CurationReviewError('review_changed', 'This review has not expired. Inspect its current questions.');
|
|
20
|
+
if ((await planKnowledgeCuration(root, { memorySyncSources: sourceIds })).planId !== baseline.planId)
|
|
21
|
+
throw new CurationReviewError('semantic_stale', 'Knowledge or evidence changed. Re-analyze before refreshing this question.');
|
|
22
|
+
const next = await createCurationReview(root, { parent: review, operationId: `question_refresh_${semanticHash([revision.id, input.decisionRevision, input.answerLogHash])}`, applied: [] }, sourceIds);
|
|
23
|
+
return graphicalReviewPayload(root, next);
|
|
24
|
+
}
|
|
9
25
|
import { sameApplicability } from './applicability.js';
|
|
10
|
-
export async function prepareSyncCuration(root, sourceIds, reviewId) {
|
|
26
|
+
export async function prepareSyncCuration(root, sourceIds, reviewId, retry = false) {
|
|
11
27
|
const previous = reviewId ? undefined : await latestCurationReview(root, (await planKnowledgeCuration(root, { memorySyncSources: sourceIds })).planId);
|
|
12
28
|
const review = reviewId ? await resumeCurationReview(root, reviewId) : previous ?? await createCurationReview(root, undefined, sourceIds);
|
|
13
29
|
const payload = await graphicalReviewPayload(root, review), g = payload.graphical;
|
|
14
30
|
if (g.activeQuestionCount || g.deferredQuestionIds.length)
|
|
15
31
|
return { reviewId: review.snapshot.reviewId, payload };
|
|
16
|
-
|
|
32
|
+
// Replaying Prepare after Retry must return its live attempt, not the old initial request.
|
|
33
|
+
if (g.continuationJobId)
|
|
34
|
+
return { reviewId: review.snapshot.reviewId, payload, job: await inspectSemanticCuration(root, g.continuationJobId) };
|
|
35
|
+
const failed = retry ? g.jobs.find(j => !j.suspended && ['rejected', 'stale'].includes(j.status)) : undefined;
|
|
36
|
+
const job = await prepareSemanticCuration(root, { reviewId: review.snapshot.reviewId, reviewRevisionId: review.snapshot.revision.id, componentId: failed?.componentId ?? review.snapshot.revision.dependencies.groups[0].component, requestId: failed ? `sync_retry_${failed.id}` : `sync_${review.snapshot.reviewId}`, reason: failed ? (failed.status === 'rejected' ? 'validation_correction' : 'resume') : 'initial', ...(failed ? failed.status === 'rejected' ? { correctionForJobId: failed.id } : { resumeForJobId: failed.id } : {}), limits: { maxItems: 24 } });
|
|
17
37
|
return { reviewId: review.snapshot.reviewId, payload, job };
|
|
18
38
|
}
|
|
19
39
|
export async function inspectSyncCuration(root, reviewId) {
|
package/dist/core/memory-sync.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
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';
|
|
2
5
|
import { previewMemoryLinkRepair, applyMemoryLinkRepair } from './memory-link-repair.js';
|
|
3
|
-
import { loadCurationReview } from './curation-review.js';
|
|
6
|
+
import { loadCurationReview, CurationReviewError } from './curation-review.js';
|
|
4
7
|
import { requireCurrentProject } from "./migrations.js";
|
|
5
8
|
import { publicDiagnostic } from './diagnostics.js';
|
|
6
|
-
import { prepareSyncCuration, inspectSyncCuration, previewSyncCuration } from './memory-sync-curator.js';
|
|
9
|
+
import { prepareSyncCuration, inspectSyncCuration, previewSyncCuration, refreshSyncQuestions } from './memory-sync-curator.js';
|
|
7
10
|
/** User-initiated orchestration. One bounded local workflow; no provider or background caller. */
|
|
8
11
|
import fs from 'node:fs/promises';
|
|
9
12
|
import { randomUUID } from 'node:crypto';
|
|
@@ -37,6 +40,7 @@ export const memorySyncActionSchema = z.discriminatedUnion('action', [
|
|
|
37
40
|
z.object({ action: z.literal('approve'), ...requestIdentity, reviewId: z.string(), approved: z.literal(true) }).strict(),
|
|
38
41
|
z.object({ action: z.literal('skip'), ...requestIdentity, candidateId: id.optional() }).strict(),
|
|
39
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(),
|
|
40
44
|
z.object({ action: z.literal('promotions'), ...requestIdentity, ids: z.array(z.string()).max(50) }).strict(),
|
|
41
45
|
z.object({ action: z.literal('complete'), ...requestIdentity }).strict(),
|
|
42
46
|
]);
|
|
@@ -45,13 +49,13 @@ async function fingerprint(root, excludeCheckpoint) { return { knowledge: await
|
|
|
45
49
|
async function load(root) { const target = await checkedDataPath(root, FILE); try {
|
|
46
50
|
const stat = await fs.stat(target);
|
|
47
51
|
if (stat.size > MEMORY_SYNC_LIMITS.bytes)
|
|
48
|
-
throw new Error('Memory
|
|
52
|
+
throw new Error('Curate Memory state exceeds bound');
|
|
49
53
|
const envelope = JSON.parse(await fs.readFile(target, 'utf8'));
|
|
50
54
|
if (envelope.hash !== digest(envelope.workflow))
|
|
51
|
-
throw new Error('Memory
|
|
55
|
+
throw new Error('Curate Memory checkpoint is corrupt');
|
|
52
56
|
const state = stateSchema.parse(envelope.workflow);
|
|
53
57
|
if (state.root !== await fs.realpath(root))
|
|
54
|
-
throw new Error('Memory
|
|
58
|
+
throw new Error('Curate Memory belongs to another project');
|
|
55
59
|
return state;
|
|
56
60
|
}
|
|
57
61
|
catch (e) {
|
|
@@ -60,7 +64,7 @@ catch (e) {
|
|
|
60
64
|
throw e;
|
|
61
65
|
} }
|
|
62
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)
|
|
63
|
-
throw new Error('Memory
|
|
67
|
+
throw new Error('Curate Memory checkpoint exceeds bounded storage; split work'); await ensureDataDirectory(root, 'local'); await writeFileAtomic(await checkedDataPath(root, FILE), text); }
|
|
64
68
|
async function promotions(root, w) { const rows = await promotionRecommendations(root, { persistCache: false }); w.promotionCandidates = rows.slice(0, 50); w.promotionTotal = rows.length; }
|
|
65
69
|
function currentJob(w) { return w.jobs.find(j => j.selected && ['pending', 'semantic_work', 'awaiting_review', 'awaiting_clarification'].includes(j.status)); }
|
|
66
70
|
async function advance(root, w) {
|
|
@@ -87,10 +91,43 @@ async function advance(root, w) {
|
|
|
87
91
|
else
|
|
88
92
|
w.state = 'completed';
|
|
89
93
|
}
|
|
90
|
-
function
|
|
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 = []) {
|
|
91
128
|
const applied = w.jobs.filter(j => j.status === 'applied').map(j => j.result.summary);
|
|
92
129
|
const promotion = w.promotionResult;
|
|
93
|
-
return { ...w, clarification, active: undefined, review: w.active ? { kind: w.active.kind, reviewId: w.active.reviewId, preview: w.active.preview } : undefined,
|
|
130
|
+
return { ...w, clarification, deferredQuestions: deferred, active: undefined, review: w.active ? { kind: w.active.kind, reviewId: w.active.reviewId, preview: w.active.preview } : undefined,
|
|
94
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) },
|
|
95
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,
|
|
96
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),
|
|
@@ -101,6 +138,34 @@ function view(w, clarification) {
|
|
|
101
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 },
|
|
102
139
|
};
|
|
103
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
|
+
}
|
|
104
169
|
async function refreshCuration(root, w) {
|
|
105
170
|
const j = currentJob(w);
|
|
106
171
|
if (!w.improveEnabled || !j?.curationReviewId || !['semantic_work', 'awaiting_clarification'].includes(w.state))
|
|
@@ -248,7 +313,7 @@ export async function runMemorySync(root, raw) {
|
|
|
248
313
|
const input = memorySyncActionSchema.parse(raw);
|
|
249
314
|
root = await findProjectRoot(root);
|
|
250
315
|
if (!await readConfig(root))
|
|
251
|
-
throw new Error('Prodero must be initialized before
|
|
316
|
+
throw new Error('Prodero must be initialized before Curate Memory');
|
|
252
317
|
if (input.action === 'start') {
|
|
253
318
|
await requireCurrentProject(root);
|
|
254
319
|
await autoMigrateProject(root);
|
|
@@ -268,20 +333,27 @@ export async function runMemorySync(root, raw) {
|
|
|
268
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 };
|
|
269
334
|
await promotions(root, w);
|
|
270
335
|
await save(root, w);
|
|
271
|
-
return
|
|
336
|
+
return workflowView(root, w);
|
|
272
337
|
});
|
|
273
338
|
}
|
|
274
339
|
return withSyncLock(root, async () => {
|
|
275
340
|
await recoverSyncTransaction(root);
|
|
276
341
|
const w = await load(root);
|
|
277
342
|
if (!w)
|
|
278
|
-
throw new Error('Start
|
|
343
|
+
throw new Error('Start Curate Memory first');
|
|
279
344
|
if (input.workflowId && input.workflowId !== w.id)
|
|
280
|
-
throw new Error('Memory
|
|
345
|
+
throw new Error('Curate Memory workflow expired or was replaced');
|
|
281
346
|
const before = w.revision;
|
|
282
347
|
await resume(root, w);
|
|
283
|
-
if (input.action === 'inspect')
|
|
284
|
-
|
|
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
|
+
}
|
|
285
357
|
if (w.state === 'stale')
|
|
286
358
|
throw new Error(w.error);
|
|
287
359
|
if (w.state === 'applying')
|
|
@@ -318,18 +390,18 @@ export async function runMemorySync(root, raw) {
|
|
|
318
390
|
throw new Error('Prepare only the next selected candidate');
|
|
319
391
|
if (w.improveEnabled) {
|
|
320
392
|
try {
|
|
321
|
-
const curation = await prepareSyncCuration(root, j.sourceIds, j.curationReviewId);
|
|
393
|
+
const curation = await prepareSyncCuration(root, j.sourceIds, j.curationReviewId, j.status === 'pending' && !!j.curationReviewId);
|
|
322
394
|
j.curationReviewId = curation.reviewId;
|
|
323
395
|
j.status = 'semantic_work';
|
|
324
396
|
await save(root, w);
|
|
325
|
-
return { ...
|
|
397
|
+
return { ...await workflowView(root, w), curation };
|
|
326
398
|
}
|
|
327
399
|
catch (e) {
|
|
328
400
|
j.status = 'failed';
|
|
329
401
|
j.error = publicDiagnostic(e).slice(0, 1000);
|
|
330
402
|
await advance(root, w);
|
|
331
403
|
await save(root, w);
|
|
332
|
-
return
|
|
404
|
+
return workflowView(root, w);
|
|
333
405
|
}
|
|
334
406
|
}
|
|
335
407
|
try {
|
|
@@ -340,7 +412,7 @@ export async function runMemorySync(root, raw) {
|
|
|
340
412
|
throw new Error('Candidate membership changed');
|
|
341
413
|
j.status = 'semantic_work';
|
|
342
414
|
await save(root, w);
|
|
343
|
-
return { ...
|
|
415
|
+
return { ...await workflowView(root, w), semantic };
|
|
344
416
|
}
|
|
345
417
|
catch (e) {
|
|
346
418
|
j.status = 'failed';
|
|
@@ -350,7 +422,7 @@ export async function runMemorySync(root, raw) {
|
|
|
350
422
|
}
|
|
351
423
|
else if (input.action === 'propose') {
|
|
352
424
|
if (w.improveEnabled)
|
|
353
|
-
throw new Error('Submit this job through Curator reasoning, then inspect
|
|
425
|
+
throw new Error('Submit this job through Curator reasoning, then inspect Curate Memory');
|
|
354
426
|
const j = currentJob(w);
|
|
355
427
|
if (w.state !== 'semantic_work' || j?.status !== 'semantic_work' || input.candidateId !== j.id || !input.proposal)
|
|
356
428
|
throw new Error('Prepare selected candidate before submitting its proposal');
|
|
@@ -370,18 +442,18 @@ export async function runMemorySync(root, raw) {
|
|
|
370
442
|
j.error = publicDiagnostic(e).slice(0, 1000);
|
|
371
443
|
await advance(root, w);
|
|
372
444
|
await save(root, w);
|
|
373
|
-
return
|
|
445
|
+
return workflowView(root, w);
|
|
374
446
|
}
|
|
375
447
|
if (w.active.preview.localDirectApplyEligible)
|
|
376
448
|
await beginApply(root, w);
|
|
377
|
-
return
|
|
449
|
+
return workflowView(root, w);
|
|
378
450
|
}
|
|
379
451
|
else if (input.action === 'approve') {
|
|
380
452
|
if (w.state !== 'awaiting_review' || !w.active || input.approved !== true || input.reviewId !== w.active.reviewId)
|
|
381
453
|
throw new Error('Explicit approval of this exact review is required');
|
|
382
454
|
w.active.approved = true;
|
|
383
455
|
await beginApply(root, w);
|
|
384
|
-
return
|
|
456
|
+
return workflowView(root, w);
|
|
385
457
|
}
|
|
386
458
|
else if (input.action === 'skip') {
|
|
387
459
|
if (w.active?.kind === 'link-repair') {
|
|
@@ -403,6 +475,15 @@ export async function runMemorySync(root, raw) {
|
|
|
403
475
|
await advance(root, w);
|
|
404
476
|
}
|
|
405
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
|
+
}
|
|
406
487
|
else if (input.action === 'retry') {
|
|
407
488
|
if (w.active || w.promotionResult)
|
|
408
489
|
throw new Error('Finish review before retry, or start a fresh workflow after promotion');
|
|
@@ -430,7 +511,7 @@ export async function runMemorySync(root, raw) {
|
|
|
430
511
|
w.state = 'completed';
|
|
431
512
|
}
|
|
432
513
|
await save(root, w);
|
|
433
|
-
return
|
|
514
|
+
return workflowView(root, w);
|
|
434
515
|
});
|
|
435
516
|
}
|
|
436
517
|
/** Open-only diagnostic projection. Never resumes, recovers, migrates or persists a workflow. */
|
|
@@ -452,12 +533,26 @@ export async function getMemorySyncOverview(root) {
|
|
|
452
533
|
/** Resolve continuation ownership from persisted review lineage, not a particular App instance. */
|
|
453
534
|
export async function syncWorkflowForCuration(root, lineageId) {
|
|
454
535
|
const w = await load(root);
|
|
455
|
-
if (!w || !w.improveEnabled ||
|
|
456
|
-
return undefined;
|
|
457
|
-
const reviewId = currentJob(w)?.curationReviewId;
|
|
458
|
-
if (!reviewId)
|
|
536
|
+
if (!w || !w.improveEnabled || w.state === 'stale')
|
|
459
537
|
return undefined;
|
|
460
|
-
const
|
|
461
|
-
|
|
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;
|
|
462
557
|
}
|
|
463
558
|
//# sourceMappingURL=memory-sync.js.map
|