@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.
- package/ARCHITECTURE.md +298 -0
- package/CHANGELOG.md +33 -0
- package/CONTRIBUTING.md +9 -0
- package/LICENSE +21 -0
- package/README.md +93 -2
- package/SECURITY.md +7 -0
- package/bin/prodero +7 -0
- package/dist/cli.js +266 -0
- package/dist/context-instructions.js +2 -0
- package/dist/contracts.js +51 -0
- package/dist/conversation-presentation.js +297 -0
- package/dist/core/agent-instructions.js +52 -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 +117 -0
- package/dist/core/curation-question.js +28 -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 +242 -0
- package/dist/core/curation-semantic-jobs.js +340 -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 +192 -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/local-preferences.js +76 -0
- package/dist/core/matching.js +6 -0
- package/dist/core/memory-index.js +241 -0
- package/dist/core/memory-intelligence.js +288 -0
- package/dist/core/memory-link-repair.js +70 -0
- package/dist/core/memory-operations.js +122 -0
- package/dist/core/memory-pressure.js +212 -0
- package/dist/core/memory-sync-curator.js +82 -0
- package/dist/core/memory-sync.js +558 -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/npm-update.js +289 -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 +116 -0
- package/dist/core/session-review.js +150 -0
- package/dist/core/sessions.js +289 -0
- package/dist/core/settings.js +94 -0
- package/dist/core/skill-curation.js +239 -0
- package/dist/core/skill-polish.js +41 -0
- package/dist/core/skills.js +387 -0
- package/dist/core/storage.js +462 -0
- package/dist/core/sync-transaction.js +290 -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 +326 -0
- package/dist/curation-review-adapter.js +92 -0
- package/dist/diagnostic-boundary.js +55 -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 +182 -0
- package/dist/memory-sync-operation.js +88 -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 +94 -0
- package/dist/presentation-trace.js +174 -0
- package/dist/presentation.js +443 -0
- package/dist/providers/mcp-sampling.js +45 -0
- package/dist/server.js +664 -0
- package/dist/settings-definition.js +22 -0
- package/dist/skill-instructions.js +2 -0
- 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 +216 -0
- package/docs/APPLICABILITY.md +151 -0
- package/docs/COMPATIBILITY.md +68 -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 +103 -0
- package/docs/PRIVACY.md +28 -0
- package/docs/RECOVERY.md +58 -0
- package/docs/RELEASING.md +98 -0
- package/docs/hosts.md +152 -0
- 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/plugins.md +83 -0
- package/docs/ui-themes.md +49 -0
- package/instructions/AGENTS.md +39 -0
- package/package.json +92 -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,569 @@
|
|
|
1
|
+
import { copyStorageTree, ensureStorageDirectory, privateStateRoot, protectPrivatePath } from "./storage.js";
|
|
2
|
+
import { writeFileExclusive } from "./storage.js";
|
|
3
|
+
import { replaceManagedBlock } from "./templates.js";
|
|
4
|
+
import { migrateKnowledgeV1ToV2, validateKnowledgeV2, migrateKnowledgeV2ToV3, validateKnowledgeV3 } from "./knowledge-migration.js";
|
|
5
|
+
import { publicDiagnostic } from "./diagnostics.js";
|
|
6
|
+
import { assertSupportedSchema, assertSupportedKnowledgeSchema } from "./storage.js";
|
|
7
|
+
import crypto from "node:crypto";
|
|
8
|
+
import fs from "node:fs/promises";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { PRODERO_DIRECTORY, PRODERO_SCHEMA_VERSION, PRODERO_KNOWLEDGE_SCHEMA_VERSION, PRODERO_VERSION } from "./constants.js";
|
|
11
|
+
import { parseFrontmatter } from "./format.js";
|
|
12
|
+
import { assertSafePath, ensureProjectDirectories, exists, projectDataPath, checkedDataPath, canonicalId, ensureDataDirectory, readConfig, validateConfig, writeFileAtomic, listCheckpoints, writeConfig } from "./storage.js";
|
|
13
|
+
import { withSyncLock, recoverSyncTransaction } from "./sync-transaction.js";
|
|
14
|
+
import { parseKnowledge } from "./memory.js";
|
|
15
|
+
import { listSessions } from "./sessions.js";
|
|
16
|
+
import { listProjectSkills } from "./skills.js";
|
|
17
|
+
import { DEFAULT_CONTEXT_BUDGET_TOKENS, DEFAULT_REQUIRED_CONTEXT_BUDGET_TOKENS } from "./telemetry.js";
|
|
18
|
+
// Schema 2 adds structured memory and richer manifests without invalidating existing Markdown/checkpoints.
|
|
19
|
+
export const MIGRATIONS = {
|
|
20
|
+
1: { from: 1, to: 2, description: "Normalize legacy canonical memory filenames to stable knowledge IDs.", invalidatesDerivedState: true, modifiesVersionedKnowledge: true, run: async (root) => {
|
|
21
|
+
const directory = await checkedDataPath(root, "memory");
|
|
22
|
+
if (!(await exists(directory)))
|
|
23
|
+
return;
|
|
24
|
+
const renames = [];
|
|
25
|
+
for (const name of (await fs.readdir(directory)).filter((entry) => entry.endsWith(".md") && !entry.startsWith("."))) {
|
|
26
|
+
const source = await checkedDataPath(root, "memory", name);
|
|
27
|
+
const { metadata } = parseFrontmatter(await fs.readFile(source, "utf8"), name);
|
|
28
|
+
if (metadata.id === undefined)
|
|
29
|
+
continue;
|
|
30
|
+
canonicalId(metadata.id, name);
|
|
31
|
+
if (`${metadata.id}.md` === name)
|
|
32
|
+
continue;
|
|
33
|
+
const destination = await checkedDataPath(root, "memory", `${metadata.id}.md`);
|
|
34
|
+
if (await exists(destination))
|
|
35
|
+
throw new Error(`Migration ID collision: ${name}`);
|
|
36
|
+
renames.push([source, destination]);
|
|
37
|
+
}
|
|
38
|
+
for (const [source, destination] of renames)
|
|
39
|
+
await fs.rename(source, destination);
|
|
40
|
+
} },
|
|
41
|
+
// Schema 3 makes the resolver budget explicit and token-based. 6,000 estimated
|
|
42
|
+
// tokens preserves the scale of the previous 24,000-character bound.
|
|
43
|
+
2: { from: 2, to: 3, description: "Replace the character context limit with an explicit estimated-token budget.", invalidatesDerivedState: true, modifiesVersionedKnowledge: false, run: async (_root, config) => {
|
|
44
|
+
config.context = { budgetTokens: config.context?.budgetTokens ?? DEFAULT_CONTEXT_BUDGET_TOKENS };
|
|
45
|
+
} },
|
|
46
|
+
// Schema 4 bounds required globals separately from the complete task-context budget.
|
|
47
|
+
3: { from: 3, to: 4, description: "Add a separate required-context budget.", invalidatesDerivedState: true, modifiesVersionedKnowledge: false, run: async (_root, config) => {
|
|
48
|
+
config.context = {
|
|
49
|
+
budgetTokens: config.context?.budgetTokens ?? DEFAULT_CONTEXT_BUDGET_TOKENS,
|
|
50
|
+
requiredBudgetTokens: config.context?.requiredBudgetTokens ?? DEFAULT_REQUIRED_CONTEXT_BUDGET_TOKENS,
|
|
51
|
+
};
|
|
52
|
+
} },
|
|
53
|
+
// Schema 5 introduces ignored Local Knowledge beside repository Versioned Knowledge.
|
|
54
|
+
// Existing canonical memory remains versioned; no knowledge is moved implicitly.
|
|
55
|
+
4: { from: 4, to: 5, description: "Add ignored Local Knowledge beside Versioned Knowledge without moving existing records.", invalidatesDerivedState: true, modifiesVersionedKnowledge: false, run: async () => { } },
|
|
56
|
+
5: { from: 5, to: 6, id: "project-5-6", kind: "structural", description: "Record the independent knowledge format; preserve all existing knowledge.", invalidatesDerivedState: true, modifiesVersionedKnowledge: false, run: async (_root, config) => { config.knowledgeSchemaVersion ??= 1; } },
|
|
57
|
+
6: { from: 6, to: 7, id: "project-instructions-6-7", kind: "structural", description: "Update managed project instructions: read and apply skills without opening the Skills UI.", affectedStores: ["AGENTS.md", "config.json"], invalidatesDerivedState: true, modifiesVersionedKnowledge: false, run: async (root) => {
|
|
58
|
+
const target = path.join(root, "AGENTS.md");
|
|
59
|
+
const existing = await fs.readFile(target, "utf8").catch(error => { if (error.code === "ENOENT")
|
|
60
|
+
return ""; throw error; });
|
|
61
|
+
await writeFileAtomic(target, replaceManagedBlock(existing));
|
|
62
|
+
} },
|
|
63
|
+
7: { from: 7, to: 8, id: "project-instructions-link-7-8", kind: "structural", description: "Replace embedded Prodero instructions with a reference to instructions supplied by the connected MCP server.", affectedStores: ["AGENTS.md", "config.json"], invalidatesDerivedState: true, modifiesVersionedKnowledge: false, run: async (root) => {
|
|
64
|
+
const target = path.join(root, "AGENTS.md");
|
|
65
|
+
const existing = await fs.readFile(target, "utf8").catch(error => { if (error.code === "ENOENT")
|
|
66
|
+
return ""; throw error; });
|
|
67
|
+
await writeFileAtomic(target, replaceManagedBlock(existing));
|
|
68
|
+
} },
|
|
69
|
+
};
|
|
70
|
+
export const KNOWLEDGE_MIGRATIONS = {
|
|
71
|
+
2: { id: 'knowledge-applicability-2-3', from: 2, to: 3, kind: 'compatible-semantic', description: 'Make applicability explicit while preserving all existing scope behavior.', affectedStores: ['memory', 'local/memory', 'archive', 'local/archive', 'config.json'], invalidatesDerivedState: true, modifiesVersionedKnowledge: true, run: async (root) => migrateKnowledgeV2ToV3(root), validate: async (root) => validateKnowledgeV3(root) },
|
|
72
|
+
1: { id: "knowledge-policy-1-2", from: 1, to: 2, kind: "compatible-semantic",
|
|
73
|
+
description: "Make required/relevant context policy explicit while preserving existing knowledge behavior and legacy authority.",
|
|
74
|
+
affectedStores: ["memory", "local/memory", "archive", "local/archive", "config.json"],
|
|
75
|
+
invalidatesDerivedState: true, modifiesVersionedKnowledge: true,
|
|
76
|
+
run: async (root) => migrateKnowledgeV1ToV2(root), validate: async (root) => validateKnowledgeV2(root),
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
export function migrationPlan(from, to = PRODERO_SCHEMA_VERSION, registry = MIGRATIONS) {
|
|
80
|
+
if (!Number.isInteger(from) || from < 1 || from > to)
|
|
81
|
+
throw new Error(`Invalid migration range v${from} → v${to}.`);
|
|
82
|
+
const steps = [];
|
|
83
|
+
for (let version = from; version < to; version += 1) {
|
|
84
|
+
const step = registry[version];
|
|
85
|
+
if (!step || step.from !== version || step.to !== version + 1)
|
|
86
|
+
throw new Error(`No migration is registered from project schema v${version}.`);
|
|
87
|
+
steps.push(step);
|
|
88
|
+
}
|
|
89
|
+
return steps;
|
|
90
|
+
}
|
|
91
|
+
async function versionedDigest(root) {
|
|
92
|
+
return (await Promise.all(["memory", "archive"].map(async (store) => directoryDigest(await checkedDataPath(root, store))))).join(":");
|
|
93
|
+
}
|
|
94
|
+
async function invalidateDerivedState(root) {
|
|
95
|
+
await Promise.all(["memory-index.json", "session-index.json", "knowledge-graph.json"].map((name) => fs.rm(projectDataPath(root, ".cache", name), { force: true })));
|
|
96
|
+
}
|
|
97
|
+
async function directoryDigest(directory) {
|
|
98
|
+
const digest = crypto.createHash("sha256");
|
|
99
|
+
const visit = async (current, relative = "") => {
|
|
100
|
+
let entries;
|
|
101
|
+
try {
|
|
102
|
+
entries = await fs.readdir(current, { withFileTypes: true });
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (error.code === "ENOENT") {
|
|
106
|
+
digest.update("missing");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
112
|
+
const childRelative = path.join(relative, entry.name), child = path.join(current, entry.name);
|
|
113
|
+
if (entry.isSymbolicLink())
|
|
114
|
+
throw new Error(`Migration cannot inspect symlinked canonical knowledge: ${childRelative}`);
|
|
115
|
+
if (entry.isDirectory()) {
|
|
116
|
+
digest.update(`D\0${childRelative}\0`);
|
|
117
|
+
await visit(child, childRelative);
|
|
118
|
+
}
|
|
119
|
+
else if (entry.isFile()) {
|
|
120
|
+
const content = await fs.readFile(child);
|
|
121
|
+
digest.update(`F\0${childRelative}\0${content.length}\0`);
|
|
122
|
+
digest.update(content);
|
|
123
|
+
digest.update("\0");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
await visit(directory);
|
|
128
|
+
return digest.digest("hex");
|
|
129
|
+
}
|
|
130
|
+
const BACKUP_ENTRIES = ["config.json", "index.md", "memory", "local", "sessions", "skills", "checkpoints", "archive"];
|
|
131
|
+
const MIGRATION_MARKER = ".transactions/migration.json";
|
|
132
|
+
async function backupDirectory(root) {
|
|
133
|
+
const directory = await ensureDataDirectory(root, ".backups");
|
|
134
|
+
await writeFileExclusive(await checkedDataPath(root, ".backups/.gitignore"), "*\n").catch(error => { if (error.code !== "EEXIST")
|
|
135
|
+
throw error; });
|
|
136
|
+
return directory;
|
|
137
|
+
}
|
|
138
|
+
async function createBackup(root, from) {
|
|
139
|
+
await backupDirectory(root);
|
|
140
|
+
const relative = `.backups/schema-${from}-${crypto.randomUUID()}`;
|
|
141
|
+
const destination = await ensureDataDirectory(root, relative);
|
|
142
|
+
const sharedModes = {};
|
|
143
|
+
const rememberSharedModes = async (source) => {
|
|
144
|
+
if (privateStateRoot(source))
|
|
145
|
+
return;
|
|
146
|
+
await assertSafePath(source);
|
|
147
|
+
const stat = await fs.lstat(source);
|
|
148
|
+
sharedModes[path.relative(projectDataPath(root), source)] = stat.mode & 0o777;
|
|
149
|
+
if (stat.isDirectory())
|
|
150
|
+
for (const name of await fs.readdir(source))
|
|
151
|
+
await rememberSharedModes(path.join(source, name));
|
|
152
|
+
};
|
|
153
|
+
for (const entry of BACKUP_ENTRIES) {
|
|
154
|
+
const source = await checkedDataPath(root, entry);
|
|
155
|
+
if (await exists(source))
|
|
156
|
+
await rememberSharedModes(source);
|
|
157
|
+
if (await exists(source))
|
|
158
|
+
await copyStorageTree(source, path.join(destination, entry));
|
|
159
|
+
}
|
|
160
|
+
const agentsPath = path.join(root, "AGENTS.md");
|
|
161
|
+
await assertSafePath(agentsPath);
|
|
162
|
+
const before = await fs.readFile(agentsPath, "utf8").catch(error => { if (error.code === "ENOENT")
|
|
163
|
+
return null; throw error; });
|
|
164
|
+
await writeFileAtomic(path.join(destination, "project-integration.json"), JSON.stringify({ before, after: replaceManagedBlock(before ?? ""), sharedModes }));
|
|
165
|
+
return relative;
|
|
166
|
+
}
|
|
167
|
+
async function restoreBackup(root, relative, expectedDigest) {
|
|
168
|
+
if (!/^\.backups\/schema-\d+-[a-f0-9-]+$/.test(relative))
|
|
169
|
+
throw new Error("Invalid migration backup path; inspect .prodero/.transactions/migration.json.");
|
|
170
|
+
const backup = await checkedDataPath(root, relative);
|
|
171
|
+
const digest = await directoryDigest(backup); // Refuse linked/incomplete recovery sources before removing any destination.
|
|
172
|
+
if (expectedDigest && digest !== expectedDigest)
|
|
173
|
+
throw new Error("Migration backup integrity check failed; preserve recovery data.");
|
|
174
|
+
if (!(await exists(path.join(backup, "config.json"))))
|
|
175
|
+
throw new Error("Migration backup is incomplete; restore manually before retrying.");
|
|
176
|
+
const backupConfig = JSON.parse(await fs.readFile(path.join(backup, "config.json"), "utf8"));
|
|
177
|
+
validateConfig(backupConfig);
|
|
178
|
+
assertSupportedSchema(backupConfig.schemaVersion);
|
|
179
|
+
assertSupportedKnowledgeSchema(backupConfig.knowledgeSchemaVersion);
|
|
180
|
+
// Older backups do not include repository-root integration files.
|
|
181
|
+
const integrationPath = path.join(backup, "project-integration.json");
|
|
182
|
+
let sharedModes;
|
|
183
|
+
if (await exists(integrationPath)) {
|
|
184
|
+
const { before, after, sharedModes: savedModes } = JSON.parse(await fs.readFile(integrationPath, "utf8"));
|
|
185
|
+
sharedModes = savedModes;
|
|
186
|
+
const target = path.join(root, "AGENTS.md");
|
|
187
|
+
await assertSafePath(target);
|
|
188
|
+
const current = await fs.readFile(target, "utf8").catch(error => { if (error.code === "ENOENT")
|
|
189
|
+
return null; throw error; });
|
|
190
|
+
if (current === after) {
|
|
191
|
+
if (before === null)
|
|
192
|
+
await fs.rm(target, { force: true });
|
|
193
|
+
else
|
|
194
|
+
await writeFileAtomic(target, before);
|
|
195
|
+
}
|
|
196
|
+
// Preserve concurrent user edits when the file is neither our preimage nor output.
|
|
197
|
+
}
|
|
198
|
+
for (const entry of BACKUP_ENTRIES)
|
|
199
|
+
await checkedDataPath(root, entry);
|
|
200
|
+
for (const entry of BACKUP_ENTRIES) {
|
|
201
|
+
const target = await checkedDataPath(root, entry), source = path.join(backup, entry);
|
|
202
|
+
// Pre-foundation journals did not snapshot the versioned archive store.
|
|
203
|
+
if (entry === "archive" && !expectedDigest && !(await exists(source)))
|
|
204
|
+
continue;
|
|
205
|
+
await fs.rm(target, { recursive: true, force: true });
|
|
206
|
+
if (await exists(source))
|
|
207
|
+
await copyStorageTree(source, target, false, (destination, sourceMode) => {
|
|
208
|
+
const saved = sharedModes?.[path.relative(projectDataPath(root), destination)];
|
|
209
|
+
// Old backups retained the original modes directly; new backups carry a private manifest.
|
|
210
|
+
if (saved === undefined)
|
|
211
|
+
return sourceMode;
|
|
212
|
+
if (!Number.isInteger(saved) || saved < 0 || saved > 0o777)
|
|
213
|
+
throw new Error("Invalid backup permission metadata");
|
|
214
|
+
return saved;
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async function validateCanonicalProject(root, schema) {
|
|
219
|
+
const knowledgeSchema = (await readConfig(root))?.knowledgeSchemaVersion ?? 1;
|
|
220
|
+
const ids = new Set();
|
|
221
|
+
for (const entry of BACKUP_ENTRIES) {
|
|
222
|
+
const target = await checkedDataPath(root, entry);
|
|
223
|
+
if (await exists(target) && (await fs.lstat(target)).isDirectory())
|
|
224
|
+
await directoryDigest(target);
|
|
225
|
+
}
|
|
226
|
+
const knowledgeDirectories = ["memory", "local/memory"];
|
|
227
|
+
for (const archive of ["archive", "local/archive"]) {
|
|
228
|
+
const dir = await checkedDataPath(root, archive);
|
|
229
|
+
if (await exists(dir))
|
|
230
|
+
for (const month of await fs.readdir(dir)) {
|
|
231
|
+
if (/^\d{4}_\d{2}$/.test(month))
|
|
232
|
+
knowledgeDirectories.push(`${archive}/${month}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
for (const directory of knowledgeDirectories) {
|
|
236
|
+
const target = await checkedDataPath(root, directory);
|
|
237
|
+
if (!(await exists(target)))
|
|
238
|
+
continue;
|
|
239
|
+
for (const name of (await fs.readdir(target)).filter(n => n.endsWith(".md") && !n.startsWith("."))) {
|
|
240
|
+
const text = await fs.readFile(await checkedDataPath(root, directory, name), "utf8");
|
|
241
|
+
const { metadata } = parseFrontmatter(text, `${directory}/${name}`);
|
|
242
|
+
if (metadata.id !== undefined)
|
|
243
|
+
canonicalId(metadata.id, `${directory}/${name}`);
|
|
244
|
+
const item = parseKnowledge(text, schema === 1 && typeof metadata.id === "string" ? `${metadata.id}.md` : name, knowledgeSchema);
|
|
245
|
+
if (ids.has(item.id))
|
|
246
|
+
throw new Error(`Duplicate canonical knowledge ID: ${directory}/${name}`);
|
|
247
|
+
ids.add(item.id);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
await listSessions(root);
|
|
251
|
+
await listProjectSkills(root);
|
|
252
|
+
await listCheckpoints(root);
|
|
253
|
+
}
|
|
254
|
+
async function validateCurrentProject(root, config) {
|
|
255
|
+
validateConfig(config);
|
|
256
|
+
if ((config.knowledgeSchemaVersion ?? 1) !== PRODERO_KNOWLEDGE_SCHEMA_VERSION)
|
|
257
|
+
throw new Error(`Migration validation expected knowledge format v${PRODERO_KNOWLEDGE_SCHEMA_VERSION}.`);
|
|
258
|
+
if (config.schemaVersion !== PRODERO_SCHEMA_VERSION)
|
|
259
|
+
throw new Error(`Migration validation expected schema v${PRODERO_SCHEMA_VERSION}.`);
|
|
260
|
+
for (const entry of ["config.json", "index.md", "memory", "local/memory", "sessions", "skills", "checkpoints", ".cache"]) {
|
|
261
|
+
if (!(await exists(await checkedDataPath(root, entry))))
|
|
262
|
+
throw new Error(`Migration validation failed: .prodero/${entry} is missing.`);
|
|
263
|
+
}
|
|
264
|
+
await validateCanonicalProject(root, config.schemaVersion);
|
|
265
|
+
}
|
|
266
|
+
async function recoverMigrationLocked(root) {
|
|
267
|
+
const marker = await checkedDataPath(root, MIGRATION_MARKER);
|
|
268
|
+
if (!(await exists(marker)))
|
|
269
|
+
return;
|
|
270
|
+
let pending;
|
|
271
|
+
try {
|
|
272
|
+
pending = JSON.parse(await fs.readFile(marker, "utf8"));
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
throw new Error("Invalid migration journal: .prodero/.transactions/migration.json; inspect before retrying.");
|
|
276
|
+
}
|
|
277
|
+
if (pending?.version !== 1 || typeof pending.backup !== "string")
|
|
278
|
+
throw new Error("Invalid migration journal: .prodero/.transactions/migration.json");
|
|
279
|
+
await restoreBackup(root, pending.backup, pending.backupDigest);
|
|
280
|
+
await invalidateDerivedState(root);
|
|
281
|
+
if (pending.history)
|
|
282
|
+
await recordHistory(root, { ...pending.history, status: "rolled-back", completedAt: new Date().toISOString() });
|
|
283
|
+
if (pending.history && /^[a-f0-9-]+$/.test(pending.history.id)) {
|
|
284
|
+
const stage = await checkedDataPath(root, `.transactions/migration-stage-${pending.history.id}`);
|
|
285
|
+
await fs.rm(stage, { recursive: true, force: true });
|
|
286
|
+
}
|
|
287
|
+
await fs.rm(marker);
|
|
288
|
+
}
|
|
289
|
+
/** Entry points recover before parsing config: rollback itself may have been interrupted. */
|
|
290
|
+
export async function recoverProjectMigration(root, expectedPlanId) {
|
|
291
|
+
if (await exists(await checkedDataPath(root, MIGRATION_MARKER)))
|
|
292
|
+
await withSyncLock(root, async () => {
|
|
293
|
+
if (expectedPlanId && (await planProjectMigration(root)).planId !== expectedPlanId)
|
|
294
|
+
throw new Error("Recovery plan changed; review it again.");
|
|
295
|
+
await recoverMigrationLocked(root);
|
|
296
|
+
}, { migrationRecovery: true });
|
|
297
|
+
}
|
|
298
|
+
export async function migrateProject(root, config, expectedPlanId) {
|
|
299
|
+
validateConfig(config);
|
|
300
|
+
assertSupportedSchema(config.schemaVersion);
|
|
301
|
+
const marker = await checkedDataPath(root, MIGRATION_MARKER);
|
|
302
|
+
const legacyBackups = await checkedDataPath(root, ".cache/migration-backups");
|
|
303
|
+
if (config.schemaVersion === PRODERO_SCHEMA_VERSION && (config.knowledgeSchemaVersion ?? 1) === PRODERO_KNOWLEDGE_SCHEMA_VERSION && !(await exists(marker)) && !(await exists(legacyBackups)))
|
|
304
|
+
return { migrated: false, from: config.schemaVersion, to: PRODERO_SCHEMA_VERSION, backup: null };
|
|
305
|
+
return withSyncLock(root, async () => {
|
|
306
|
+
if (expectedPlanId && (await planProjectMigration(root)).planId !== expectedPlanId)
|
|
307
|
+
throw new Error("Migration plan changed; review it again.");
|
|
308
|
+
await recoverMigrationLocked(root);
|
|
309
|
+
await recoverSyncTransaction(root);
|
|
310
|
+
const current = await readConfig(root);
|
|
311
|
+
if (!current)
|
|
312
|
+
throw new Error("Migration requires .prodero/config.json.");
|
|
313
|
+
const from = current.schemaVersion;
|
|
314
|
+
const plan = [...migrationPlan(from).map(step => ({ ...step, domain: "project" })), ...migrationPlan(current.knowledgeSchemaVersion ?? 1, PRODERO_KNOWLEDGE_SCHEMA_VERSION, KNOWLEDGE_MIGRATIONS).map(step => ({ ...step, domain: "knowledge" }))];
|
|
315
|
+
if (!expectedPlanId && plan.some(step => step.kind === "compatible-semantic"))
|
|
316
|
+
throw new Error("Explicit approval of the knowledge migration plan is required.");
|
|
317
|
+
if (plan.some(step => step.kind === "review-required"))
|
|
318
|
+
throw new Error("Migration requires semantic review; this runtime has no review resolver for these steps.");
|
|
319
|
+
// Preserve old-runtime backups too before their former cache directory is discarded.
|
|
320
|
+
if (await exists(legacyBackups)) {
|
|
321
|
+
await directoryDigest(legacyBackups);
|
|
322
|
+
await backupDirectory(root);
|
|
323
|
+
await fs.rename(legacyBackups, await checkedDataPath(root, `.backups/legacy-cache-${crypto.randomUUID()}`));
|
|
324
|
+
}
|
|
325
|
+
if (!plan.length) {
|
|
326
|
+
Object.assign(config, current);
|
|
327
|
+
return { migrated: false, from, to: PRODERO_SCHEMA_VERSION, backup: null };
|
|
328
|
+
}
|
|
329
|
+
await validateCanonicalProject(root, from);
|
|
330
|
+
const backup = await createBackup(root, from);
|
|
331
|
+
const history = { id: crypto.randomUUID(), curatorVersion: PRODERO_VERSION, from: { project: from, knowledge: current.knowledgeSchemaVersion ?? 1 }, to: { project: PRODERO_SCHEMA_VERSION, knowledge: PRODERO_KNOWLEDGE_SCHEMA_VERSION }, migrations: plan.map(step => step.id ?? `${step.domain}-${step.from}-${step.to}`), startedAt: new Date().toISOString(), backup, status: "started" };
|
|
332
|
+
const backupDigest = await directoryDigest(projectDataPath(root, backup));
|
|
333
|
+
const stage = projectDataPath(root, `.transactions/migration-stage-${history.id}`);
|
|
334
|
+
await recordHistory(root, history);
|
|
335
|
+
await writeFileAtomic(marker, JSON.stringify({ version: 1, backup, backupDigest, history }));
|
|
336
|
+
const next = structuredClone(current);
|
|
337
|
+
try {
|
|
338
|
+
await ensureStorageDirectory(path.join(stage, PRODERO_DIRECTORY));
|
|
339
|
+
for (const entry of BACKUP_ENTRIES) {
|
|
340
|
+
const source = projectDataPath(root, `${backup}/${entry}`);
|
|
341
|
+
if (await exists(source))
|
|
342
|
+
await copyStorageTree(source, projectDataPath(stage, entry));
|
|
343
|
+
}
|
|
344
|
+
const integration = JSON.parse(await fs.readFile(projectDataPath(root, `${backup}/project-integration.json`), "utf8"));
|
|
345
|
+
if (integration.before !== null)
|
|
346
|
+
await writeFileAtomic(path.join(stage, "AGENTS.md"), integration.before);
|
|
347
|
+
await ensureProjectDirectories(stage);
|
|
348
|
+
for (const migration of plan) {
|
|
349
|
+
const versionedBefore = migration.modifiesVersionedKnowledge ? null : await versionedDigest(stage);
|
|
350
|
+
await migration.run(stage, next);
|
|
351
|
+
if (versionedBefore !== null && await versionedDigest(stage) !== versionedBefore)
|
|
352
|
+
throw new Error(`Migration v${migration.from} → v${migration.to} modified Versioned Knowledge without declaring it.`);
|
|
353
|
+
if (migration.domain === "project")
|
|
354
|
+
next.schemaVersion = migration.to;
|
|
355
|
+
else
|
|
356
|
+
next.knowledgeSchemaVersion = migration.to;
|
|
357
|
+
validateConfig(next);
|
|
358
|
+
await writeConfig(stage, next);
|
|
359
|
+
await validateCanonicalProject(stage, next.schemaVersion);
|
|
360
|
+
await migration.validate?.(stage, next);
|
|
361
|
+
}
|
|
362
|
+
next.lastUpdatedWith = PRODERO_VERSION;
|
|
363
|
+
await validateCurrentProject(stage, next);
|
|
364
|
+
// Rebuild against the target schema in staging, never interpret migrated
|
|
365
|
+
// records using the still-old live config. Derived file stamps are checked
|
|
366
|
+
// again by the normal cache loaders after installation.
|
|
367
|
+
const { buildIndex } = await import("./index.js");
|
|
368
|
+
const { discoverSessions } = await import("./session-index.js");
|
|
369
|
+
await buildIndex(stage);
|
|
370
|
+
await discoverSessions(stage);
|
|
371
|
+
// The existing journal and common writer lock make this a recoverable logical
|
|
372
|
+
// transaction. Each file replacement is atomic; readers are fenced until done.
|
|
373
|
+
for (const entry of BACKUP_ENTRIES.filter(entry => entry !== "config.json")) {
|
|
374
|
+
const target = await checkedDataPath(root, entry), source = projectDataPath(stage, entry);
|
|
375
|
+
await commitTree(source, target);
|
|
376
|
+
}
|
|
377
|
+
await invalidateDerivedState(root);
|
|
378
|
+
for (const name of ["memory-index.json", "session-index.json", "knowledge-graph.json"]) {
|
|
379
|
+
await commitTree(projectDataPath(stage, ".cache", name), await checkedDataPath(root, ".cache", name));
|
|
380
|
+
}
|
|
381
|
+
if (plan.some(step => step.affectedStores?.includes("AGENTS.md"))) {
|
|
382
|
+
const target = path.join(root, "AGENTS.md");
|
|
383
|
+
await assertSafePath(target);
|
|
384
|
+
const currentAgents = await fs.readFile(target, "utf8").catch(error => { if (error.code === "ENOENT")
|
|
385
|
+
return null; throw error; });
|
|
386
|
+
if (currentAgents !== integration.before)
|
|
387
|
+
throw new Error("AGENTS.md changed during migration; retry with the current file.");
|
|
388
|
+
await commitTree(path.join(stage, "AGENTS.md"), target);
|
|
389
|
+
}
|
|
390
|
+
await writeConfig(root, next);
|
|
391
|
+
await recordHistory(root, { ...history, status: "completed", completedAt: new Date().toISOString() });
|
|
392
|
+
await fs.rm(stage, { recursive: true, force: true });
|
|
393
|
+
await fs.rm(marker);
|
|
394
|
+
}
|
|
395
|
+
catch (error) {
|
|
396
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
397
|
+
try {
|
|
398
|
+
await restoreBackup(root, backup, backupDigest);
|
|
399
|
+
await invalidateDerivedState(root);
|
|
400
|
+
await recordHistory(root, { ...history, status: "rolled-back", completedAt: new Date().toISOString(), error: message });
|
|
401
|
+
await fs.rm(marker);
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
throw new Error(`${message} Migration recovery required: preserve .prodero/${backup} and .prodero/.transactions/migration.json; retry migration or restore the backup manually.`);
|
|
405
|
+
}
|
|
406
|
+
await fs.rm(stage, { recursive: true, force: true });
|
|
407
|
+
throw new Error(`${message} Migration rolled back; recovery backup: .prodero/${backup}.`);
|
|
408
|
+
}
|
|
409
|
+
Object.assign(config, next);
|
|
410
|
+
return { migrated: true, from, to: PRODERO_SCHEMA_VERSION, backup: `${PRODERO_DIRECTORY}/${backup}` };
|
|
411
|
+
}, { migrationRecovery: true });
|
|
412
|
+
}
|
|
413
|
+
/** Replace each file by same-directory rename; the journal fences the multi-file commit. */
|
|
414
|
+
async function commitTree(source, target) {
|
|
415
|
+
await assertSafePath(target);
|
|
416
|
+
if (!(await exists(source))) {
|
|
417
|
+
await fs.rm(target, { recursive: true, force: true });
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const stat = await fs.lstat(source);
|
|
421
|
+
if (stat.isSymbolicLink())
|
|
422
|
+
throw new Error("Symlink in staged migration data");
|
|
423
|
+
if (stat.isDirectory()) {
|
|
424
|
+
await ensureStorageDirectory(target);
|
|
425
|
+
const names = await fs.readdir(source);
|
|
426
|
+
for (const name of await fs.readdir(target))
|
|
427
|
+
if (!names.includes(name))
|
|
428
|
+
await fs.rm(path.join(target, name), { recursive: true, force: true });
|
|
429
|
+
for (const name of names)
|
|
430
|
+
await commitTree(path.join(source, name), path.join(target, name));
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
const temporary = `${target}.tmp-${crypto.randomUUID()}`;
|
|
434
|
+
try {
|
|
435
|
+
await copyStorageTree(source, temporary);
|
|
436
|
+
// Staging/backup files are private; do not propagate their modes to shared targets.
|
|
437
|
+
if (!privateStateRoot(target) && process.platform !== "win32") {
|
|
438
|
+
const existing = await fs.lstat(target).catch(error => { if (error.code === "ENOENT")
|
|
439
|
+
return null; throw error; });
|
|
440
|
+
if (existing)
|
|
441
|
+
await fs.chmod(temporary, existing.mode & 0o777);
|
|
442
|
+
}
|
|
443
|
+
await fs.rename(temporary, target);
|
|
444
|
+
await protectPrivatePath(target);
|
|
445
|
+
}
|
|
446
|
+
finally {
|
|
447
|
+
await fs.rm(temporary, { force: true });
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
async function recordHistory(root, entry) {
|
|
452
|
+
if (!/^[a-f0-9-]+$/.test(entry.id))
|
|
453
|
+
throw new Error("Invalid migration history ID");
|
|
454
|
+
await ensureDataDirectory(root, ".migrations");
|
|
455
|
+
await writeFileAtomic(await checkedDataPath(root, ".migrations/.gitignore"), "*\n");
|
|
456
|
+
await writeFileAtomic(await checkedDataPath(root, `.migrations/${entry.id}.json`), JSON.stringify(entry, null, 2) + "\n");
|
|
457
|
+
}
|
|
458
|
+
/** Only metadata is inspected. No directories, caches, sessions or journals are written. */
|
|
459
|
+
export async function planProjectMigration(root) {
|
|
460
|
+
const plan = { root, state: "current", current: null, target: { project: PRODERO_SCHEMA_VERSION, knowledge: PRODERO_KNOWLEDGE_SCHEMA_VERSION }, curatorVersion: PRODERO_VERSION, migrations: [], backupRequired: false, safeToApply: false, reviewRequired: false, blocked: false, planId: "" };
|
|
461
|
+
const configPath = projectDataPath(root, "config.json");
|
|
462
|
+
await assertSafePath(configPath);
|
|
463
|
+
const raw = await fs.readFile(configPath, "utf8").catch(error => { if (error.code === "ENOENT")
|
|
464
|
+
return null; throw error; });
|
|
465
|
+
plan.planId = crypto.createHash("sha256").update(raw ?? "uninitialized").update(JSON.stringify(plan.target)).update(PRODERO_VERSION).digest("hex");
|
|
466
|
+
const marker = projectDataPath(root, MIGRATION_MARKER);
|
|
467
|
+
await assertSafePath(marker);
|
|
468
|
+
const journal = await fs.readFile(marker, "utf8").catch(error => { if (error.code === "ENOENT")
|
|
469
|
+
return null; throw error; });
|
|
470
|
+
plan.planId = crypto.createHash("sha256").update(plan.planId).update(journal ?? "no-migration").digest("hex");
|
|
471
|
+
try {
|
|
472
|
+
let config;
|
|
473
|
+
try {
|
|
474
|
+
config = raw === null ? null : JSON.parse(raw);
|
|
475
|
+
}
|
|
476
|
+
catch (error) {
|
|
477
|
+
if (await exists(marker)) {
|
|
478
|
+
plan.state = "recovery-required";
|
|
479
|
+
plan.blocked = true;
|
|
480
|
+
plan.recovery = "Restore the pre-update backup.";
|
|
481
|
+
return plan;
|
|
482
|
+
}
|
|
483
|
+
throw error;
|
|
484
|
+
}
|
|
485
|
+
if (config) {
|
|
486
|
+
if (!Number.isInteger(config.schemaVersion) || config.schemaVersion < 1 || !Number.isInteger(config.knowledgeSchemaVersion ?? 1) || (config.knowledgeSchemaVersion ?? 1) < 1)
|
|
487
|
+
throw new Error("PRODERO_INVALID_CONFIG: Invalid schema version metadata.");
|
|
488
|
+
plan.current = { project: config.schemaVersion, knowledge: config.knowledgeSchemaVersion ?? 1 };
|
|
489
|
+
if (plan.current.project > plan.target.project || plan.current.knowledge > plan.target.knowledge) {
|
|
490
|
+
plan.state = "runtime-too-old";
|
|
491
|
+
plan.blocked = true;
|
|
492
|
+
plan.diagnostic = `${plan.current.project > plan.target.project ? "PRODERO_SCHEMA_NEWER" : "PRODERO_KNOWLEDGE_SCHEMA_NEWER"}: Update Curator; the project uses a newer project or knowledge format.`;
|
|
493
|
+
return plan;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (await exists(marker)) {
|
|
497
|
+
const ownerPath = projectDataPath(root, ".transactions/lock/owner");
|
|
498
|
+
await assertSafePath(ownerPath);
|
|
499
|
+
try {
|
|
500
|
+
const pid = Number(await fs.readFile(ownerPath, "utf8"));
|
|
501
|
+
if (Number.isSafeInteger(pid) && pid > 0) {
|
|
502
|
+
process.kill(pid, 0);
|
|
503
|
+
plan.state = "updating";
|
|
504
|
+
plan.blocked = true;
|
|
505
|
+
return plan;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
catch { }
|
|
509
|
+
plan.state = "recovery-required";
|
|
510
|
+
plan.blocked = true;
|
|
511
|
+
plan.recovery = "Restore the complete pre-update backup, then plan the update again.";
|
|
512
|
+
return plan;
|
|
513
|
+
}
|
|
514
|
+
if (!config)
|
|
515
|
+
return plan;
|
|
516
|
+
validateConfig(config);
|
|
517
|
+
for (const domain of ["project", "knowledge"]) {
|
|
518
|
+
for (const step of migrationPlan(plan.current[domain], plan.target[domain], domain === "project" ? MIGRATIONS : KNOWLEDGE_MIGRATIONS))
|
|
519
|
+
plan.migrations.push({ id: step.id ?? `${domain}-${step.from}-${step.to}`, domain, from: step.from, to: step.to, kind: step.kind ?? "structural", description: step.description, affectedStores: step.affectedStores ?? ["config.json", ...(step.invalidatesDerivedState ? ["index.md", "local/index.md", ".cache"] : []), ...(step.modifiesVersionedKnowledge ? ["memory", "local/memory", "archive", "local/archive"] : [])] });
|
|
520
|
+
}
|
|
521
|
+
plan.reviewRequired = plan.migrations.some(s => s.kind === "review-required");
|
|
522
|
+
plan.backupRequired = plan.migrations.length > 0;
|
|
523
|
+
plan.state = plan.reviewRequired ? "review-required" : plan.backupRequired ? "available" : "current";
|
|
524
|
+
plan.safeToApply = plan.state === "available";
|
|
525
|
+
plan.blocked = plan.reviewRequired;
|
|
526
|
+
plan.planId = crypto.createHash("sha256").update(plan.planId).update(JSON.stringify(plan.migrations)).digest("hex");
|
|
527
|
+
}
|
|
528
|
+
catch (error) {
|
|
529
|
+
plan.state = "blocked";
|
|
530
|
+
plan.blocked = true;
|
|
531
|
+
plan.diagnostic = publicDiagnostic(error);
|
|
532
|
+
}
|
|
533
|
+
return plan;
|
|
534
|
+
}
|
|
535
|
+
export class MigrationRequiredError extends Error {
|
|
536
|
+
plan;
|
|
537
|
+
constructor(plan) {
|
|
538
|
+
super(`PRODERO_MIGRATION_REQUIRED: ${plan.state}. Open Prodero to review the knowledge update.`);
|
|
539
|
+
this.plan = plan;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
export async function requireCurrentProject(root) {
|
|
543
|
+
const plan = await planProjectMigration(root);
|
|
544
|
+
if (plan.state !== "current")
|
|
545
|
+
throw new MigrationRequiredError(plan);
|
|
546
|
+
}
|
|
547
|
+
/** User approval is bound to the exact metadata/registry plan. Review-required steps
|
|
548
|
+
* intentionally cannot be overridden by a boolean approval. */
|
|
549
|
+
export async function applyProjectMigration(root, options) {
|
|
550
|
+
if (!options.approved)
|
|
551
|
+
throw new Error("Explicit approval of the knowledge update is required.");
|
|
552
|
+
const plan = await planProjectMigration(root);
|
|
553
|
+
if (plan.state === "current")
|
|
554
|
+
return { migrated: false, plan };
|
|
555
|
+
if (plan.planId !== options.planId)
|
|
556
|
+
throw new Error("Migration plan changed; review the current plan again.");
|
|
557
|
+
if (plan.state === "recovery-required" && options.recover) {
|
|
558
|
+
await recoverProjectMigration(root, options.planId);
|
|
559
|
+
return { migrated: false, recovered: true, plan: await planProjectMigration(root) };
|
|
560
|
+
}
|
|
561
|
+
if (!plan.safeToApply)
|
|
562
|
+
throw new Error(plan.diagnostic ?? `Migration is ${plan.state}; cannot apply automatically.`);
|
|
563
|
+
const config = await readConfig(root);
|
|
564
|
+
if (!config)
|
|
565
|
+
throw new Error("Project is not initialized.");
|
|
566
|
+
const result = await migrateProject(root, config, options.planId);
|
|
567
|
+
return { ...result, plan: await planProjectMigration(root) };
|
|
568
|
+
}
|
|
569
|
+
//# sourceMappingURL=migrations.js.map
|