@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.
- package/ARCHITECTURE.md +298 -0
- package/CHANGELOG.md +16 -0
- package/CONTRIBUTING.md +9 -0
- package/LICENSE +21 -0
- package/README.md +94 -2
- package/SECURITY.md +7 -0
- package/bin/prodero +7 -0
- package/dist/cli.js +228 -0
- package/dist/context-instructions.js +2 -0
- package/dist/contracts.js +50 -0
- package/dist/conversation-presentation.js +262 -0
- package/dist/core/agent-instructions.js +39 -0
- package/dist/core/applicability-label.js +11 -0
- package/dist/core/applicability.js +143 -0
- package/dist/core/bootstrap.js +129 -0
- package/dist/core/consolidation.js +315 -0
- package/dist/core/constants.js +21 -0
- package/dist/core/context-allocation.js +54 -0
- package/dist/core/context-snapshot.js +12 -0
- package/dist/core/context.js +489 -0
- package/dist/core/curation-clauses.js +68 -0
- package/dist/core/curation-evidence.js +98 -0
- package/dist/core/curation-graphical.js +105 -0
- package/dist/core/curation-review-presentation.js +4 -0
- package/dist/core/curation-review-state.js +62 -0
- package/dist/core/curation-review.js +573 -0
- package/dist/core/curation-revision.js +106 -0
- package/dist/core/curation-semantic-contract.js +239 -0
- package/dist/core/curation-semantic-jobs.js +280 -0
- package/dist/core/diagnostics.js +17 -0
- package/dist/core/format.js +114 -0
- package/dist/core/ids.js +6 -0
- package/dist/core/imports.js +169 -0
- package/dist/core/index.js +93 -0
- package/dist/core/instruction-review.js +86 -0
- package/dist/core/integrity-files.js +174 -0
- package/dist/core/intelligence.js +95 -0
- package/dist/core/knowledge-analysis.js +46 -0
- package/dist/core/knowledge-authority.js +143 -0
- package/dist/core/knowledge-curation.js +316 -0
- package/dist/core/knowledge-engine.js +475 -0
- package/dist/core/knowledge-graph.js +191 -0
- package/dist/core/knowledge-lifecycle.js +212 -0
- package/dist/core/knowledge-management.js +113 -0
- package/dist/core/knowledge-migration.js +84 -0
- package/dist/core/knowledge-policy.js +38 -0
- package/dist/core/knowledge-provenance.js +90 -0
- package/dist/core/knowledge-write-policy.js +85 -0
- package/dist/core/language.js +14 -0
- package/dist/core/matching.js +6 -0
- package/dist/core/memory-index.js +239 -0
- package/dist/core/memory-intelligence.js +288 -0
- package/dist/core/memory-link-repair.js +66 -0
- package/dist/core/memory-operations.js +122 -0
- package/dist/core/memory-pressure.js +212 -0
- package/dist/core/memory-sync-curator.js +62 -0
- package/dist/core/memory-sync.js +463 -0
- package/dist/core/memory-transformation-bundle.js +163 -0
- package/dist/core/memory-transformation-proposal.js +135 -0
- package/dist/core/memory-transformation.js +108 -0
- package/dist/core/memory.js +186 -0
- package/dist/core/migrations.js +569 -0
- package/dist/core/path-scope.js +27 -0
- package/dist/core/plugin-files.js +116 -0
- package/dist/core/project-root.js +39 -0
- package/dist/core/project.js +634 -0
- package/dist/core/promotion.js +279 -0
- package/dist/core/reconciliation.js +61 -0
- package/dist/core/secrets.js +67 -0
- package/dist/core/semantic-safety.js +33 -0
- package/dist/core/session-index.js +112 -0
- package/dist/core/session-review.js +150 -0
- package/dist/core/sessions.js +289 -0
- package/dist/core/settings.js +76 -0
- package/dist/core/skill-curation.js +239 -0
- package/dist/core/skill-polish.js +41 -0
- package/dist/core/skills.js +368 -0
- package/dist/core/storage.js +408 -0
- package/dist/core/sync-transaction.js +254 -0
- package/dist/core/telemetry.js +64 -0
- package/dist/core/templates.js +53 -0
- package/dist/core/types.js +2 -0
- package/dist/core/update.js +320 -0
- package/dist/curation-review-adapter.js +92 -0
- package/dist/diagnostic-boundary.js +54 -0
- package/dist/filesystem/permissions.js +39 -0
- package/dist/hosts/adapters.js +119 -0
- package/dist/hosts/files.js +133 -0
- package/dist/hosts/manager.js +150 -0
- package/dist/hosts/public-entry.js +47 -0
- package/dist/hosts/runtime-command.js +66 -0
- package/dist/hosts/runtime.js +139 -0
- package/dist/hosts/wizard.js +114 -0
- package/dist/knowledge-table.js +38 -0
- package/dist/mcp-development.js +69 -0
- package/dist/mcp-workspace.js +141 -0
- package/dist/memory-presentation.js +172 -0
- package/dist/plugins/api.js +2 -0
- package/dist/plugins/context.js +20 -0
- package/dist/plugins/mcp.js +72 -0
- package/dist/plugins/registry.js +106 -0
- package/dist/presentation-cards.js +32 -0
- package/dist/presentation-server.js +79 -0
- package/dist/presentation-trace.js +174 -0
- package/dist/presentation.js +412 -0
- package/dist/providers/mcp-sampling.js +45 -0
- package/dist/server.js +656 -0
- package/dist/settings-definition.js +22 -0
- package/dist/skill-instructions.js +2 -0
- package/dist-ui/index.html +214 -0
- package/docs/APPLICABILITY.md +151 -0
- package/docs/COMPATIBILITY.md +49 -0
- package/docs/CONTEXT-SELECTION-V2.md +61 -0
- package/docs/CURATION-APP-REVIEW.md +156 -0
- package/docs/CURATION-CLAUSE-REFINEMENT.md +45 -0
- package/docs/IMPORTER-CURATION.md +142 -0
- package/docs/KNOWLEDGE-AUTHORITY.md +165 -0
- package/docs/KNOWLEDGE-SCHEMA-V2.md +150 -0
- package/docs/MIGRATIONS.md +99 -0
- package/docs/PRIVACY.md +28 -0
- package/docs/RECOVERY.md +56 -0
- package/docs/RELEASING.md +40 -0
- package/docs/hosts.md +134 -0
- package/docs/plugins.md +83 -0
- package/docs/ui-themes.md +43 -0
- package/instructions/AGENTS.md +33 -0
- package/package.json +88 -4
- package/plugins/migrate_ai_studio/README.md +101 -0
- package/plugins/migrate_ai_studio/index.cjs +396 -0
- package/plugins/migrate_ai_studio/instructions/audit.md +25 -0
- package/plugins/migrate_ai_studio/instructions/classify.md +41 -0
- package/plugins/migrate_ai_studio/instructions/migrate.md +34 -0
- package/plugins/migrate_ai_studio/instructions/verify.md +20 -0
- package/plugins/migrate_ai_studio/plugin.json +11 -0
- package/plugins/migrate_ai_studio/schema.cjs +20 -0
- package/plugins/migrate_ai_studio/ui/dist/index.html +77 -0
- package/prodero.release.json +5 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
import { loadKnowledgeGraph } from './knowledge-graph.js';
|
|
2
|
+
import { previewMemoryLinkRepair, applyMemoryLinkRepair } from './memory-link-repair.js';
|
|
3
|
+
import { loadCurationReview } from './curation-review.js';
|
|
4
|
+
import { requireCurrentProject } from "./migrations.js";
|
|
5
|
+
import { publicDiagnostic } from './diagnostics.js';
|
|
6
|
+
import { prepareSyncCuration, inspectSyncCuration, previewSyncCuration } from './memory-sync-curator.js';
|
|
7
|
+
/** User-initiated orchestration. One bounded local workflow; no provider or background caller. */
|
|
8
|
+
import fs from 'node:fs/promises';
|
|
9
|
+
import { randomUUID } from 'node:crypto';
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import { checkedDataPath, ensureDataDirectory, writeFileAtomic, sha256, listCheckpoints, readConfig } from './storage.js';
|
|
12
|
+
import { withSyncLock, recoverSyncTransaction, fileHash } from './sync-transaction.js';
|
|
13
|
+
import { readKnowledgeInventory } from './knowledge-provenance.js';
|
|
14
|
+
import { getMemoryIntelligencePlan } from './memory-intelligence.js';
|
|
15
|
+
import { prepareMemoryTransformation, validateMemoryTransformationProposal, memoryTransformationProposalSchema } from './memory-transformation-proposal.js';
|
|
16
|
+
import { previewMemoryTransformation, applyMemoryTransformation } from './memory-transformation.js';
|
|
17
|
+
import { promotionRecommendations, previewKnowledgePromotion, applyKnowledgePromotion } from './promotion.js';
|
|
18
|
+
import { discoverSessions } from './session-index.js';
|
|
19
|
+
import { syncProject, autoMigrateProject, findProjectRoot } from './project.js';
|
|
20
|
+
const FILE = 'local/memory-sync.json';
|
|
21
|
+
export const MEMORY_SYNC_LIMITS = { workflows: 1, candidates: 20, promotions: 50, bytes: 4 * 1024 * 1024 };
|
|
22
|
+
const digest = (v) => sha256(JSON.stringify(v));
|
|
23
|
+
const id = z.string().regex(/^area_[a-f0-9]{20}$/);
|
|
24
|
+
const fingerprintSchema = z.object({ knowledge: z.string(), telemetry: z.string(), checkpoints: z.string() }).strict();
|
|
25
|
+
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();
|
|
26
|
+
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(),
|
|
27
|
+
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(),
|
|
28
|
+
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(),
|
|
29
|
+
}).strict();
|
|
30
|
+
const requestIdentity = { workflowId: z.string().uuid(), revision: z.number().int().min(1) };
|
|
31
|
+
export const memorySyncActionSchema = z.discriminatedUnion('action', [
|
|
32
|
+
z.object({ action: z.literal('start'), restart: z.boolean().optional() }).strict(),
|
|
33
|
+
z.object({ action: z.literal('inspect'), workflowId: z.string().uuid().optional() }).strict(),
|
|
34
|
+
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(),
|
|
35
|
+
z.object({ action: z.literal('prepare'), ...requestIdentity, candidateId: id }).strict(),
|
|
36
|
+
z.object({ action: z.literal('propose'), ...requestIdentity, candidateId: id, proposal: memoryTransformationProposalSchema }).strict(),
|
|
37
|
+
z.object({ action: z.literal('approve'), ...requestIdentity, reviewId: z.string(), approved: z.literal(true) }).strict(),
|
|
38
|
+
z.object({ action: z.literal('skip'), ...requestIdentity, candidateId: id.optional() }).strict(),
|
|
39
|
+
z.object({ action: z.literal('retry'), ...requestIdentity, candidateId: id }).strict(),
|
|
40
|
+
z.object({ action: z.literal('promotions'), ...requestIdentity, ids: z.array(z.string()).max(50) }).strict(),
|
|
41
|
+
z.object({ action: z.literal('complete'), ...requestIdentity }).strict(),
|
|
42
|
+
]);
|
|
43
|
+
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])); }
|
|
44
|
+
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)])) }; }
|
|
45
|
+
async function load(root) { const target = await checkedDataPath(root, FILE); try {
|
|
46
|
+
const stat = await fs.stat(target);
|
|
47
|
+
if (stat.size > MEMORY_SYNC_LIMITS.bytes)
|
|
48
|
+
throw new Error('Memory Sync state exceeds bound');
|
|
49
|
+
const envelope = JSON.parse(await fs.readFile(target, 'utf8'));
|
|
50
|
+
if (envelope.hash !== digest(envelope.workflow))
|
|
51
|
+
throw new Error('Memory Sync checkpoint is corrupt');
|
|
52
|
+
const state = stateSchema.parse(envelope.workflow);
|
|
53
|
+
if (state.root !== await fs.realpath(root))
|
|
54
|
+
throw new Error('Memory Sync belongs to another project');
|
|
55
|
+
return state;
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
if (e.code === 'ENOENT')
|
|
59
|
+
return null;
|
|
60
|
+
throw e;
|
|
61
|
+
} }
|
|
62
|
+
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 Sync checkpoint exceeds bounded storage; split work'); await ensureDataDirectory(root, 'local'); await writeFileAtomic(await checkedDataPath(root, FILE), text); }
|
|
64
|
+
async function promotions(root, w) { const rows = await promotionRecommendations(root, { persistCache: false }); w.promotionCandidates = rows.slice(0, 50); w.promotionTotal = rows.length; }
|
|
65
|
+
function currentJob(w) { return w.jobs.find(j => j.selected && ['pending', 'semantic_work', 'awaiting_review', 'awaiting_clarification'].includes(j.status)); }
|
|
66
|
+
async function advance(root, w) {
|
|
67
|
+
if (w.repairLinksEnabled && !w.repairLinksDone) {
|
|
68
|
+
const preview = await previewMemoryLinkRepair(root);
|
|
69
|
+
if (preview.count) {
|
|
70
|
+
w.active = { kind: 'link-repair', reviewId: preview.previewId, preview, approved: false };
|
|
71
|
+
w.state = 'awaiting_review';
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
w.repairLinksDone = true;
|
|
75
|
+
}
|
|
76
|
+
if (currentJob(w))
|
|
77
|
+
w.state = 'semantic_work';
|
|
78
|
+
else if (w.promotionEnabled && !w.promotionDone) {
|
|
79
|
+
await promotions(root, w);
|
|
80
|
+
if (w.improveEnabled !== undefined && !w.promotionCandidates.length) {
|
|
81
|
+
w.promotionDone = true;
|
|
82
|
+
w.state = 'completed';
|
|
83
|
+
}
|
|
84
|
+
else
|
|
85
|
+
w.state = 'awaiting_promotion_selection';
|
|
86
|
+
}
|
|
87
|
+
else
|
|
88
|
+
w.state = 'completed';
|
|
89
|
+
}
|
|
90
|
+
function view(w, clarification) {
|
|
91
|
+
const applied = w.jobs.filter(j => j.status === 'applied').map(j => j.result.summary);
|
|
92
|
+
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,
|
|
94
|
+
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
|
+
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
|
+
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),
|
|
97
|
+
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),
|
|
98
|
+
estimatedTokens: { scope: 'transformed_areas', before: applied.reduce((n, r) => n + r.estimatedTokens.before, 0), after: applied.reduce((n, r) => n + r.estimatedTokens.after, 0) },
|
|
99
|
+
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 },
|
|
100
|
+
coverage: { promotionCandidatesOmitted: Math.max(0, w.promotionTotal - w.promotionCandidates.length) },
|
|
101
|
+
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
|
+
};
|
|
103
|
+
}
|
|
104
|
+
async function refreshCuration(root, w) {
|
|
105
|
+
const j = currentJob(w);
|
|
106
|
+
if (!w.improveEnabled || !j?.curationReviewId || !['semantic_work', 'awaiting_clarification'].includes(w.state))
|
|
107
|
+
return;
|
|
108
|
+
const before = digest(w);
|
|
109
|
+
let payload;
|
|
110
|
+
try {
|
|
111
|
+
payload = await inspectSyncCuration(root, j.curationReviewId);
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
j.status = 'failed';
|
|
115
|
+
j.error = publicDiagnostic(e).slice(0, 1000);
|
|
116
|
+
await advance(root, w);
|
|
117
|
+
await save(root, w);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const g = payload.graphical;
|
|
121
|
+
j.curationReviewId = payload.review.snapshot.reviewId;
|
|
122
|
+
j.curationStateHash = digest([j.curationReviewId, payload.review.state.revision, g.answerLogHash, g.jobs]);
|
|
123
|
+
j.unresolved = payload.projection.unresolvedIssueCount;
|
|
124
|
+
if (g.analysisPending) {
|
|
125
|
+
const answering = g.questions.some(q => q.state === 'answered_pending_analysis');
|
|
126
|
+
w.state = answering ? 'awaiting_clarification' : 'semantic_work';
|
|
127
|
+
j.status = w.state;
|
|
128
|
+
if (digest(w) !== before)
|
|
129
|
+
await save(root, w);
|
|
130
|
+
return answering ? payload : undefined;
|
|
131
|
+
}
|
|
132
|
+
if (g.activeQuestionCount || g.analysisFailed && g.questions.some(q => q.state === 'answered_pending_analysis')) {
|
|
133
|
+
w.state = 'awaiting_clarification';
|
|
134
|
+
j.status = 'awaiting_clarification';
|
|
135
|
+
j.unresolved = payload.projection.unresolvedIssueCount;
|
|
136
|
+
if (digest(w) !== before)
|
|
137
|
+
await save(root, w);
|
|
138
|
+
return payload;
|
|
139
|
+
}
|
|
140
|
+
if (g.deferredQuestionIds.length || payload.projection.paused) {
|
|
141
|
+
j.status = 'deferred';
|
|
142
|
+
j.unresolved = payload.projection.unresolvedIssueCount;
|
|
143
|
+
await advance(root, w);
|
|
144
|
+
await save(root, w);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (g.analysisFailed) {
|
|
148
|
+
j.status = 'failed';
|
|
149
|
+
j.error = 'Analysis could not finish; saved answers are retained.';
|
|
150
|
+
await advance(root, w);
|
|
151
|
+
await save(root, w);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const typed = payload.review.snapshot.revision.typedPlan;
|
|
155
|
+
if (typed.kind !== 'semantic-v2')
|
|
156
|
+
return payload;
|
|
157
|
+
let preview;
|
|
158
|
+
try {
|
|
159
|
+
preview = await previewSyncCuration(root, j.id, payload);
|
|
160
|
+
}
|
|
161
|
+
catch (e) {
|
|
162
|
+
j.status = 'failed';
|
|
163
|
+
j.error = publicDiagnostic(e).slice(0, 1000);
|
|
164
|
+
await advance(root, w);
|
|
165
|
+
await save(root, w);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (!preview) {
|
|
169
|
+
j.status = 'skipped';
|
|
170
|
+
j.unresolved = payload.projection.unresolvedIssueCount;
|
|
171
|
+
j.error = 'Knowledge retained because the available evidence did not support a safe change.';
|
|
172
|
+
await advance(root, w);
|
|
173
|
+
await save(root, w);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (!preview.localDirectApplyEligible)
|
|
177
|
+
throw new Error('Curator Sync result is not eligible for automatic Local apply');
|
|
178
|
+
w.active = { kind: 'transformation', candidateId: j.id, reviewId: preview.previewId, preview, approved: false };
|
|
179
|
+
await beginApply(root, w);
|
|
180
|
+
}
|
|
181
|
+
async function finishApply(root, w) {
|
|
182
|
+
const a = w.active;
|
|
183
|
+
if ((a.kind === 'promotion' || a.kind === 'link-repair') && !a.approved)
|
|
184
|
+
throw new Error('This operation requires persisted exact workflow approval');
|
|
185
|
+
let result;
|
|
186
|
+
if (a.kind === 'transformation') {
|
|
187
|
+
const p = a.preview;
|
|
188
|
+
result = await applyMemoryTransformation(root, p, { initiatedByUser: true, reviewedPreviewId: p.previewId, approvedVersioned: a.approved, approvedSemanticLimitations: a.approved });
|
|
189
|
+
w.jobs.find(j => j.id === a.candidateId).status = 'applied';
|
|
190
|
+
w.jobs.find(j => j.id === a.candidateId).result = result;
|
|
191
|
+
}
|
|
192
|
+
else if (a.kind === 'link-repair') {
|
|
193
|
+
result = await applyMemoryLinkRepair(root, a.preview, a.approved);
|
|
194
|
+
w.repairLinksResult = { fixed: result.fixed, remaining: result.remaining };
|
|
195
|
+
w.repairLinksDone = true;
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
result = await applyKnowledgePromotion(root, a.preview, true);
|
|
199
|
+
w.promotionResult = result;
|
|
200
|
+
w.promotionDone = true;
|
|
201
|
+
}
|
|
202
|
+
// A crash after commit must not silently adopt unrelated external changes on resume.
|
|
203
|
+
const now = await fingerprint(root, result.checkpoint);
|
|
204
|
+
const revisions = a.kind === 'transformation' ? a.preview.lifecycle.changes.map(c => ({ path: c.to, hash: sha256(c.after) })) : result.revisions;
|
|
205
|
+
const unchanged = now.telemetry === w.baseline.telemetry && now.checkpoints === w.baseline.checkpoints && await knowledgeHash(root, a.touchedIds) === a.unaffectedKnowledge;
|
|
206
|
+
const exact = (await Promise.all(revisions.map(async (r) => await fileHash(await checkedDataPath(root, r.path)) === r.hash))).every(Boolean);
|
|
207
|
+
delete w.active;
|
|
208
|
+
if (!unchanged || !exact) {
|
|
209
|
+
w.state = 'stale';
|
|
210
|
+
w.error = 'Operation committed, but project changed independently. Start a fresh workflow.';
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
w.baseline = await fingerprint(root);
|
|
214
|
+
await advance(root, w);
|
|
215
|
+
}
|
|
216
|
+
await save(root, w);
|
|
217
|
+
}
|
|
218
|
+
async function beginApply(root, w) {
|
|
219
|
+
const a = w.active;
|
|
220
|
+
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] : [])]);
|
|
221
|
+
a.unaffectedKnowledge = await knowledgeHash(root, a.touchedIds);
|
|
222
|
+
w.state = 'applying';
|
|
223
|
+
await save(root, w); // authorization/intention durable BEFORE knowledge journal
|
|
224
|
+
await finishApply(root, w);
|
|
225
|
+
}
|
|
226
|
+
async function resume(root, w) {
|
|
227
|
+
if (w.state === 'applying') {
|
|
228
|
+
try {
|
|
229
|
+
await finishApply(root, w);
|
|
230
|
+
}
|
|
231
|
+
catch (e) {
|
|
232
|
+
w.error = publicDiagnostic(e).slice(0, 1000);
|
|
233
|
+
if (/stale|changed|mismatch/i.test(w.error) && !await fileHash(await checkedDataPath(root, '.transactions/pending.json')))
|
|
234
|
+
w.state = 'stale';
|
|
235
|
+
await save(root, w);
|
|
236
|
+
}
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (w.state === 'stale')
|
|
240
|
+
return;
|
|
241
|
+
if (digest(await fingerprint(root)) !== digest(w.baseline)) {
|
|
242
|
+
w.state = 'stale';
|
|
243
|
+
w.error = 'Knowledge, telemetry or checkpoints changed. Start a fresh workflow; old approvals cannot be reused.';
|
|
244
|
+
await save(root, w);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
export async function runMemorySync(root, raw) {
|
|
248
|
+
const input = memorySyncActionSchema.parse(raw);
|
|
249
|
+
root = await findProjectRoot(root);
|
|
250
|
+
if (!await readConfig(root))
|
|
251
|
+
throw new Error('Prodero must be initialized before Sync Memory');
|
|
252
|
+
if (input.action === 'start') {
|
|
253
|
+
await requireCurrentProject(root);
|
|
254
|
+
await autoMigrateProject(root);
|
|
255
|
+
// Explicit start recovers observations only when pending; no semantic maintenance on open.
|
|
256
|
+
const existing = await load(root);
|
|
257
|
+
if (existing && !('restart' in input && input.restart) && !['completed', 'stale'].includes(existing.state))
|
|
258
|
+
return runMemorySync(root, { action: 'inspect', workflowId: existing.id });
|
|
259
|
+
let recovery = { sessions: 0, created: 0 };
|
|
260
|
+
await withSyncLock(root, () => recoverSyncTransaction(root));
|
|
261
|
+
const discovery = await discoverSessions(root, { loadPendingBodies: false });
|
|
262
|
+
if (discovery.sessions.some(s => s.pending)) {
|
|
263
|
+
const synced = await syncProject(root);
|
|
264
|
+
recovery = { sessions: synced.sessions, created: synced.created };
|
|
265
|
+
}
|
|
266
|
+
return withSyncLock(root, async () => {
|
|
267
|
+
const plan = await getMemoryIntelligencePlan(root);
|
|
268
|
+
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
|
+
await promotions(root, w);
|
|
270
|
+
await save(root, w);
|
|
271
|
+
return view(w);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return withSyncLock(root, async () => {
|
|
275
|
+
await recoverSyncTransaction(root);
|
|
276
|
+
const w = await load(root);
|
|
277
|
+
if (!w)
|
|
278
|
+
throw new Error('Start Sync Memory first');
|
|
279
|
+
if (input.workflowId && input.workflowId !== w.id)
|
|
280
|
+
throw new Error('Memory Sync workflow expired or was replaced');
|
|
281
|
+
const before = w.revision;
|
|
282
|
+
await resume(root, w);
|
|
283
|
+
if (input.action === 'inspect')
|
|
284
|
+
return view(w, w.state !== 'stale' ? await refreshCuration(root, w) : undefined);
|
|
285
|
+
if (w.state === 'stale')
|
|
286
|
+
throw new Error(w.error);
|
|
287
|
+
if (w.state === 'applying')
|
|
288
|
+
throw new Error('Approved apply is pending recovery; inspect/resume before another action');
|
|
289
|
+
if (!('revision' in input) || input.revision !== before || w.revision !== before)
|
|
290
|
+
throw new Error('Stale workflow revision; inspect current workflow');
|
|
291
|
+
delete w.error;
|
|
292
|
+
if (input.action === 'select') {
|
|
293
|
+
if (w.state !== 'awaiting_selection')
|
|
294
|
+
throw new Error('Selection is already fixed');
|
|
295
|
+
const modern = input.candidateIds === undefined && input.reviewPromotion === undefined;
|
|
296
|
+
if (!modern && (input.improve !== undefined || input.share !== undefined || input.repairLinks !== undefined))
|
|
297
|
+
throw new Error('Choose operations or legacy candidates, not both');
|
|
298
|
+
const improve = input.improve ?? true, share = input.share ?? true, repairLinks = Boolean(w.brokenLinks) && (input.repairLinks ?? true);
|
|
299
|
+
if (modern && !improve && !share && !repairLinks)
|
|
300
|
+
throw new Error('Select Improve memory, Fix broken links or Share knowledge');
|
|
301
|
+
const ids = modern ? (improve ? w.jobs.map(j => j.id) : []) : input.candidateIds ?? [];
|
|
302
|
+
if (new Set(ids).size !== ids.length || ids.some(id => !w.jobs.some(j => j.id === id)))
|
|
303
|
+
throw new Error('Unknown/duplicate candidate selection');
|
|
304
|
+
if (modern) {
|
|
305
|
+
w.improveEnabled = improve;
|
|
306
|
+
w.repairLinksEnabled = repairLinks;
|
|
307
|
+
}
|
|
308
|
+
for (const j of w.jobs) {
|
|
309
|
+
j.selected = ids.includes(j.id);
|
|
310
|
+
j.status = j.selected ? 'pending' : 'unselected';
|
|
311
|
+
}
|
|
312
|
+
w.promotionEnabled = modern ? share : input.reviewPromotion === true;
|
|
313
|
+
await advance(root, w);
|
|
314
|
+
}
|
|
315
|
+
else if (input.action === 'prepare') {
|
|
316
|
+
const j = currentJob(w);
|
|
317
|
+
if (w.state !== 'semantic_work' || !j || input.candidateId !== j.id)
|
|
318
|
+
throw new Error('Prepare only the next selected candidate');
|
|
319
|
+
if (w.improveEnabled) {
|
|
320
|
+
try {
|
|
321
|
+
const curation = await prepareSyncCuration(root, j.sourceIds, j.curationReviewId);
|
|
322
|
+
j.curationReviewId = curation.reviewId;
|
|
323
|
+
j.status = 'semantic_work';
|
|
324
|
+
await save(root, w);
|
|
325
|
+
return { ...view(w), curation };
|
|
326
|
+
}
|
|
327
|
+
catch (e) {
|
|
328
|
+
j.status = 'failed';
|
|
329
|
+
j.error = publicDiagnostic(e).slice(0, 1000);
|
|
330
|
+
await advance(root, w);
|
|
331
|
+
await save(root, w);
|
|
332
|
+
return view(w);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
const semantic = await prepareMemoryTransformation(root, j.id);
|
|
337
|
+
if (!semantic.canPropose)
|
|
338
|
+
throw new Error('Candidate requires splitting or complete evidence');
|
|
339
|
+
if (semantic.bundle.sourceIds.length !== j.recordCount || semantic.bundle.sourceIds.some(id => !j.sourceIds.includes(id)))
|
|
340
|
+
throw new Error('Candidate membership changed');
|
|
341
|
+
j.status = 'semantic_work';
|
|
342
|
+
await save(root, w);
|
|
343
|
+
return { ...view(w), semantic };
|
|
344
|
+
}
|
|
345
|
+
catch (e) {
|
|
346
|
+
j.status = 'failed';
|
|
347
|
+
j.error = publicDiagnostic(e).slice(0, 1000);
|
|
348
|
+
await advance(root, w);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
else if (input.action === 'propose') {
|
|
352
|
+
if (w.improveEnabled)
|
|
353
|
+
throw new Error('Submit this job through Curator reasoning, then inspect Sync Memory');
|
|
354
|
+
const j = currentJob(w);
|
|
355
|
+
if (w.state !== 'semantic_work' || j?.status !== 'semantic_work' || input.candidateId !== j.id || !input.proposal)
|
|
356
|
+
throw new Error('Prepare selected candidate before submitting its proposal');
|
|
357
|
+
try {
|
|
358
|
+
if (input.proposal.candidateId !== j.id)
|
|
359
|
+
throw new Error('Proposal belongs to another candidate');
|
|
360
|
+
const validated = await validateMemoryTransformationProposal(root, input.proposal);
|
|
361
|
+
const preview = await previewMemoryTransformation(root, validated);
|
|
362
|
+
j.unresolved = preview.summary.unresolved.length;
|
|
363
|
+
w.active = { kind: 'transformation', candidateId: j.id, reviewId: preview.previewId, preview, approved: false };
|
|
364
|
+
j.status = 'awaiting_review';
|
|
365
|
+
w.state = 'awaiting_review';
|
|
366
|
+
await save(root, w);
|
|
367
|
+
}
|
|
368
|
+
catch (e) {
|
|
369
|
+
j.status = 'failed';
|
|
370
|
+
j.error = publicDiagnostic(e).slice(0, 1000);
|
|
371
|
+
await advance(root, w);
|
|
372
|
+
await save(root, w);
|
|
373
|
+
return view(w);
|
|
374
|
+
}
|
|
375
|
+
if (w.active.preview.localDirectApplyEligible)
|
|
376
|
+
await beginApply(root, w);
|
|
377
|
+
return view(w);
|
|
378
|
+
}
|
|
379
|
+
else if (input.action === 'approve') {
|
|
380
|
+
if (w.state !== 'awaiting_review' || !w.active || input.approved !== true || input.reviewId !== w.active.reviewId)
|
|
381
|
+
throw new Error('Explicit approval of this exact review is required');
|
|
382
|
+
w.active.approved = true;
|
|
383
|
+
await beginApply(root, w);
|
|
384
|
+
return view(w);
|
|
385
|
+
}
|
|
386
|
+
else if (input.action === 'skip') {
|
|
387
|
+
if (w.active?.kind === 'link-repair') {
|
|
388
|
+
delete w.active;
|
|
389
|
+
w.repairLinksDone = true;
|
|
390
|
+
await advance(root, w);
|
|
391
|
+
}
|
|
392
|
+
else if (w.active?.kind === 'promotion') {
|
|
393
|
+
delete w.active;
|
|
394
|
+
w.promotionDone = true;
|
|
395
|
+
await advance(root, w);
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
const j = currentJob(w);
|
|
399
|
+
if (!j || input.candidateId !== j.id)
|
|
400
|
+
throw new Error('Skip requires current candidate');
|
|
401
|
+
j.status = 'skipped';
|
|
402
|
+
delete w.active;
|
|
403
|
+
await advance(root, w);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
else if (input.action === 'retry') {
|
|
407
|
+
if (w.active || w.promotionResult)
|
|
408
|
+
throw new Error('Finish review before retry, or start a fresh workflow after promotion');
|
|
409
|
+
const j = w.jobs.find(j => j.id === input.candidateId && j.selected && j.status === 'failed');
|
|
410
|
+
if (!j)
|
|
411
|
+
throw new Error('Only failed candidates can be retried');
|
|
412
|
+
j.status = 'pending';
|
|
413
|
+
delete j.error;
|
|
414
|
+
w.state = 'semantic_work';
|
|
415
|
+
}
|
|
416
|
+
else if (input.action === 'promotions') {
|
|
417
|
+
if (w.state !== 'awaiting_promotion_selection' || !w.promotionEnabled)
|
|
418
|
+
throw new Error('Finish selected transformations before promotion review');
|
|
419
|
+
const ids = input.ids ?? [];
|
|
420
|
+
if (!ids.length || new Set(ids).size !== ids.length || ids.some(id => !w.promotionCandidates.some(c => c.id === id)))
|
|
421
|
+
throw new Error('Select specific current Local promotion candidates');
|
|
422
|
+
const preview = await previewKnowledgePromotion(root, { ids });
|
|
423
|
+
w.active = { kind: 'promotion', reviewId: preview.previewId, preview, approved: false };
|
|
424
|
+
w.state = 'awaiting_review';
|
|
425
|
+
}
|
|
426
|
+
else if (input.action === 'complete') {
|
|
427
|
+
if (!['awaiting_promotion_selection', 'completed'].includes(w.state))
|
|
428
|
+
throw new Error('Finish or skip selected transformations first');
|
|
429
|
+
w.promotionDone = true;
|
|
430
|
+
w.state = 'completed';
|
|
431
|
+
}
|
|
432
|
+
await save(root, w);
|
|
433
|
+
return view(w);
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
/** Open-only diagnostic projection. Never resumes, recovers, migrates or persists a workflow. */
|
|
437
|
+
export async function getMemorySyncOverview(root) {
|
|
438
|
+
const workflow = await load(root);
|
|
439
|
+
const pending = workflow && workflow.state !== 'completed' ? workflow : null;
|
|
440
|
+
const stale = pending && (pending.state === 'stale' || pending.state !== 'applying' && digest(await fingerprint(root)) !== digest(pending.baseline));
|
|
441
|
+
const workflowStatus = pending ? { id: pending.id, state: stale ? 'stale' : pending.state } : null;
|
|
442
|
+
try {
|
|
443
|
+
const plan = await getMemoryIntelligencePlan(root);
|
|
444
|
+
return { available: plan.coverage.telemetry !== 'unavailable', sessions: plan.coverage.sessions,
|
|
445
|
+
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 })),
|
|
446
|
+
areasOmitted: plan.truncated.candidates, workflow: workflowStatus };
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
return { available: false, sessions: 0, areas: [], areasOmitted: 0, workflow: workflowStatus };
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
/** Resolve continuation ownership from persisted review lineage, not a particular App instance. */
|
|
453
|
+
export async function syncWorkflowForCuration(root, lineageId) {
|
|
454
|
+
const w = await load(root);
|
|
455
|
+
if (!w || !w.improveEnabled || !['semantic_work', 'awaiting_clarification'].includes(w.state))
|
|
456
|
+
return undefined;
|
|
457
|
+
const reviewId = currentJob(w)?.curationReviewId;
|
|
458
|
+
if (!reviewId)
|
|
459
|
+
return undefined;
|
|
460
|
+
const review = await loadCurationReview(root, reviewId);
|
|
461
|
+
return review.snapshot.revision?.lineageId === lineageId ? w.id : undefined;
|
|
462
|
+
}
|
|
463
|
+
//# sourceMappingURL=memory-sync.js.map
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { readConfig } from './storage.js';
|
|
2
|
+
/** Temporary evidence packaging only. No prompt, proposal, writer or provider. */
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { getMemoryIntelligenceCandidate } from './memory-intelligence.js';
|
|
7
|
+
import { parseKnowledge } from './memory.js';
|
|
8
|
+
import { checkedDataPath, sha256 } from './storage.js';
|
|
9
|
+
import { estimateTokens, TOKEN_COUNT_METHOD } from './telemetry.js';
|
|
10
|
+
export const MEMORY_TRANSFORMATION_BUNDLE_LIMITS = { activeSources: 24, historicalSources: 8, provenanceEdges: 128, totalTokens: 32768, totalBytes: 131072, evidenceTokens: 12000 };
|
|
11
|
+
const limitsSchema = z.object({
|
|
12
|
+
activeSources: z.number().int().min(1).max(24).default(24), historicalSources: z.number().int().min(0).max(8).default(8),
|
|
13
|
+
provenanceEdges: z.number().int().min(0).max(128).default(128), totalTokens: z.number().int().min(2048).max(32768).default(32768),
|
|
14
|
+
totalBytes: z.number().int().min(8192).max(131072).default(131072), evidenceTokens: z.number().int().min(1).max(12000).default(12000),
|
|
15
|
+
}).strict();
|
|
16
|
+
const protectedCategories = ['instruction', 'principle', 'invariant', 'constraint', 'decision'];
|
|
17
|
+
const sortIds = (ids) => [...new Set(ids)].sort();
|
|
18
|
+
const encodedBytes = (value) => Buffer.byteLength(JSON.stringify(value), 'utf8');
|
|
19
|
+
/** One current planner candidate; no unrequested neighbors or recursive archive bodies. */
|
|
20
|
+
export async function buildMemoryTransformationBundle(root, candidateId, options = {}) {
|
|
21
|
+
const limits = limitsSchema.parse(options.limits ?? {});
|
|
22
|
+
const current = await getMemoryIntelligenceCandidate(root, candidateId);
|
|
23
|
+
if (options.expectedCandidateRevision && options.expectedCandidateRevision !== current.revision)
|
|
24
|
+
throw new Error('Stale memory candidate revision');
|
|
25
|
+
const { candidate, sourceIds, provenance, locations } = current;
|
|
26
|
+
const activeIds = new Set(sourceIds), locationById = new Map(locations.map(r => [r.id, r]));
|
|
27
|
+
const historicalIds = sortIds(provenance.filter(e => activeIds.has(e.from) && !activeIds.has(e.to)).map(e => e.to));
|
|
28
|
+
const loaded = new Map();
|
|
29
|
+
const load = async (id, role) => {
|
|
30
|
+
const location = locationById.get(id);
|
|
31
|
+
if (!location)
|
|
32
|
+
return null;
|
|
33
|
+
const text = await fs.readFile(await checkedDataPath(root, location.path), 'utf8');
|
|
34
|
+
const knowledge = parseKnowledge(text, path.basename(location.path), (await readConfig(root))?.knowledgeSchemaVersion);
|
|
35
|
+
if (knowledge.id !== id)
|
|
36
|
+
throw new Error('Knowledge identity changed while building bundle');
|
|
37
|
+
if (role === 'active_source' && (location.storage !== 'current' || knowledge.archivedAt || !['active', 'disputed'].includes(knowledge.status)))
|
|
38
|
+
throw new Error('Stale active bundle source');
|
|
39
|
+
const record = { role, id, layer: location.layer, path: location.path, revision: sha256(text), lifecycle: { storage: location.storage, current: role === 'active_source', status: knowledge.status, archivedAt: knowledge.archivedAt ?? null },
|
|
40
|
+
protected: protectedCategories.includes(knowledge.category), knowledge, sourceMarkdown: text };
|
|
41
|
+
loaded.set(id, record);
|
|
42
|
+
return record;
|
|
43
|
+
};
|
|
44
|
+
const evidenceFits = estimateTokens(JSON.stringify(candidate.evidence)) <= limits.evidenceTokens;
|
|
45
|
+
const bundle = {
|
|
46
|
+
version: 1, bundleId: 'bundle_' + '0'.repeat(64), candidateId, status: 'ready', readyForSemanticWork: true,
|
|
47
|
+
limits, sourceIds, activeSources: [], historicalSources: [],
|
|
48
|
+
planner: { revision: current.revision, policy: current.policy, coverage: current.coverage,
|
|
49
|
+
label: candidate.label, recordCount: candidate.recordCount, signals: candidate.signals, priority: candidate.priority, counts: candidate.counts, layers: candidate.layers, categories: candidate.categories,
|
|
50
|
+
containsProtectedKnowledge: candidate.containsProtectedKnowledge, reconciliation: candidate.reconciliation, truncated: candidate.truncated, evidence: evidenceFits ? candidate.evidence : null },
|
|
51
|
+
protection: { categories: protectedCategories, policy: 'conservative_handling_required', transformationAuthorized: false },
|
|
52
|
+
provenance: { bodyExpansionDepth: 1, edges: provenance.slice(0, limits.provenanceEdges), edgesDropped: Math.max(0, provenance.length - limits.provenanceEdges), referenceOnlyIds: [] },
|
|
53
|
+
omissions: { activeSourceIds: [], historicalSourceIds: [], missingHistoricalIds: [], plannerEvidence: !evidenceFits, reasons: [] },
|
|
54
|
+
revisions: { active: {}, historical: {} },
|
|
55
|
+
size: { bytes: 0, estimatedTokens: 0, tokenCountMethod: TOKEN_COUNT_METHOD, estimated: true },
|
|
56
|
+
};
|
|
57
|
+
// Reserve envelope accounting/digest space. All serialized metadata and escaped bodies count.
|
|
58
|
+
const fits = () => encodedBytes(bundle) + 256 <= Math.min(limits.totalBytes, limits.totalTokens * 4);
|
|
59
|
+
if (sourceIds.length > limits.activeSources)
|
|
60
|
+
bundle.omissions.reasons.push('active_record_limit');
|
|
61
|
+
if (!evidenceFits)
|
|
62
|
+
bundle.omissions.reasons.push('planner_evidence_limit');
|
|
63
|
+
if (!bundle.omissions.reasons.length) {
|
|
64
|
+
for (const id of sourceIds) {
|
|
65
|
+
const record = await load(id, 'active_source');
|
|
66
|
+
if (!record)
|
|
67
|
+
throw new Error(`Missing active bundle source: ${id}`);
|
|
68
|
+
bundle.activeSources.push(record);
|
|
69
|
+
bundle.revisions.active[id] = record.revision;
|
|
70
|
+
if (!fits()) {
|
|
71
|
+
bundle.omissions.reasons.push('active_content_budget');
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (bundle.omissions.reasons.length) {
|
|
77
|
+
// An incomplete active cluster is never handed out as a usable semantic package.
|
|
78
|
+
bundle.activeSources = [];
|
|
79
|
+
bundle.revisions.active = {};
|
|
80
|
+
bundle.omissions.activeSourceIds = [...sourceIds];
|
|
81
|
+
bundle.status = 'requires_split';
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
for (const id of historicalIds) {
|
|
85
|
+
if (bundle.historicalSources.length >= limits.historicalSources) {
|
|
86
|
+
bundle.omissions.historicalSourceIds.push(id);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const record = await load(id, 'historical_provenance');
|
|
90
|
+
if (!record) {
|
|
91
|
+
bundle.omissions.historicalSourceIds.push(id);
|
|
92
|
+
bundle.omissions.missingHistoricalIds.push(id);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
bundle.historicalSources.push(record);
|
|
96
|
+
bundle.revisions.historical[id] = record.revision;
|
|
97
|
+
if (!fits()) {
|
|
98
|
+
bundle.historicalSources.pop();
|
|
99
|
+
delete bundle.revisions.historical[id];
|
|
100
|
+
bundle.omissions.historicalSourceIds.push(id);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (bundle.status === 'requires_split')
|
|
105
|
+
bundle.omissions.historicalSourceIds = [...historicalIds];
|
|
106
|
+
const included = new Set([...bundle.activeSources, ...bundle.historicalSources].map(r => r.id));
|
|
107
|
+
bundle.provenance.referenceOnlyIds = sortIds(bundle.provenance.edges.flatMap(e => [e.from, e.to]).filter(id => !included.has(id)));
|
|
108
|
+
if (bundle.omissions.historicalSourceIds.length)
|
|
109
|
+
bundle.omissions.reasons.push('direct_history_incomplete');
|
|
110
|
+
if (bundle.provenance.edgesDropped)
|
|
111
|
+
bundle.omissions.reasons.push('provenance_edge_limit');
|
|
112
|
+
if (bundle.status !== 'requires_split' && bundle.omissions.reasons.length)
|
|
113
|
+
bundle.status = 'bundle_truncated';
|
|
114
|
+
bundle.readyForSemanticWork = bundle.status === 'ready';
|
|
115
|
+
// Omission manifests/metadata also consume space. Never escape the total budget with them.
|
|
116
|
+
if (!fits()) {
|
|
117
|
+
bundle.status = 'requires_split';
|
|
118
|
+
bundle.readyForSemanticWork = false;
|
|
119
|
+
bundle.activeSources = [];
|
|
120
|
+
bundle.historicalSources = [];
|
|
121
|
+
bundle.revisions = { active: {}, historical: {} };
|
|
122
|
+
bundle.omissions.activeSourceIds = [...sourceIds];
|
|
123
|
+
bundle.omissions.historicalSourceIds = [...historicalIds];
|
|
124
|
+
bundle.omissions.reasons.push('envelope_budget');
|
|
125
|
+
bundle.provenance.referenceOnlyIds = sortIds(bundle.provenance.edges.flatMap(e => [e.from, e.to]));
|
|
126
|
+
if (bundle.planner.evidence) {
|
|
127
|
+
bundle.planner.evidence = null;
|
|
128
|
+
bundle.omissions.plannerEvidence = true;
|
|
129
|
+
}
|
|
130
|
+
if (!fits())
|
|
131
|
+
throw new Error('BUNDLE_MANIFEST_TOO_LARGE: candidate IDs/metadata cannot fit the bounded diagnostic; candidate requires splitting. No bundle produced.');
|
|
132
|
+
}
|
|
133
|
+
const fresh = await getMemoryIntelligenceCandidate(root, candidateId);
|
|
134
|
+
if (fresh.revision !== current.revision)
|
|
135
|
+
throw new Error('Memory candidate changed during bundle construction; retry');
|
|
136
|
+
for (const record of loaded.values())
|
|
137
|
+
if (sha256(await fs.readFile(await checkedDataPath(root, record.path), 'utf8')) !== record.revision)
|
|
138
|
+
throw new Error('Knowledge changed during bundle construction; retry');
|
|
139
|
+
bundle.bundleId = bundleDigest(bundle);
|
|
140
|
+
for (let i = 0; i < 4; i++) {
|
|
141
|
+
const bytes = encodedBytes(bundle);
|
|
142
|
+
const tokens = Math.ceil(bytes / 4);
|
|
143
|
+
if (bundle.size.bytes === bytes && bundle.size.estimatedTokens === tokens)
|
|
144
|
+
break;
|
|
145
|
+
bundle.size.bytes = bytes;
|
|
146
|
+
bundle.size.estimatedTokens = tokens;
|
|
147
|
+
}
|
|
148
|
+
if (bundle.size.bytes > limits.totalBytes || bundle.size.estimatedTokens > limits.totalTokens)
|
|
149
|
+
throw new Error('Bundle exceeds total envelope budget');
|
|
150
|
+
return bundle;
|
|
151
|
+
}
|
|
152
|
+
function bundleDigest(bundle) {
|
|
153
|
+
const { bundleId: _, size: __, ...payload } = bundle;
|
|
154
|
+
return `bundle_${sha256(JSON.stringify(payload))}`;
|
|
155
|
+
}
|
|
156
|
+
/** Read-only validity check; hashes are revision tokens, not publication authorization. */
|
|
157
|
+
export async function validateMemoryTransformationBundle(root, bundle) {
|
|
158
|
+
if (bundle.version !== 1 || bundleDigest(bundle) !== bundle.bundleId || encodedBytes(bundle) !== bundle.size.bytes || Math.ceil(encodedBytes(bundle) / 4) !== bundle.size.estimatedTokens)
|
|
159
|
+
return { valid: false, reason: 'modified_bundle' };
|
|
160
|
+
const fresh = await buildMemoryTransformationBundle(root, bundle.candidateId, { limits: bundle.limits });
|
|
161
|
+
return fresh.bundleId === bundle.bundleId ? { valid: true, reason: 'current' } : { valid: false, reason: 'stale_evidence' };
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=memory-transformation-bundle.js.map
|