@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,462 @@
|
|
|
1
|
+
import { PRIVATE_DIRECTORY_MODE, PRIVATE_FILE_MODE, tightenPrivateMode } from "../filesystem/permissions.js";
|
|
2
|
+
import { presentationConfigSchema } from "../settings-definition.js";
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { PRODERO_DIRECTORY, PROJECT_DIRECTORIES, PRODERO_SCHEMA_VERSION, PRODERO_KNOWLEDGE_SCHEMA_VERSION } from "./constants.js";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
const LOCAL_STATE_DIRECTORIES = ["local", "sessions", "checkpoints", ".cache", ".transactions", ".backups", ".migrations"];
|
|
9
|
+
export const LOCAL_STATE_IGNORE_RULES = [
|
|
10
|
+
...LOCAL_STATE_DIRECTORIES.flatMap(directory => ["sessions", "checkpoints"].includes(directory)
|
|
11
|
+
? [`${PRODERO_DIRECTORY}/${directory}/*`, `!${PRODERO_DIRECTORY}/${directory}/.gitkeep`]
|
|
12
|
+
: [`${PRODERO_DIRECTORY}/${directory}/`]),
|
|
13
|
+
// Atomic scratch files are never Versioned content, even next to a Versioned target.
|
|
14
|
+
`${PRODERO_DIRECTORY}/**/*.tmp-*`,
|
|
15
|
+
];
|
|
16
|
+
const ignoreUpdates = new Map();
|
|
17
|
+
/** Install directory-wide exclusions before creating Local/runtime state. Preserve user rules/files. */
|
|
18
|
+
export async function ensureLocalStateIgnored(root) {
|
|
19
|
+
const target = path.resolve(root, ".gitignore");
|
|
20
|
+
const pending = ignoreUpdates.get(target);
|
|
21
|
+
if (pending)
|
|
22
|
+
return pending;
|
|
23
|
+
const update = (async () => {
|
|
24
|
+
await assertSafePath(target);
|
|
25
|
+
const existing = await fs.readFile(target, "utf8").catch(error => { if (error.code === "ENOENT")
|
|
26
|
+
return ""; throw error; });
|
|
27
|
+
const lines = existing.split(/\r?\n/);
|
|
28
|
+
const missing = LOCAL_STATE_IGNORE_RULES.filter(rule => !lines.includes(rule));
|
|
29
|
+
if (missing.length)
|
|
30
|
+
await writeFileAtomic(target, `${existing}${existing && !existing.endsWith("\n") ? "\n" : ""}${missing.join("\n")}\n`);
|
|
31
|
+
})();
|
|
32
|
+
ignoreUpdates.set(target, update);
|
|
33
|
+
try {
|
|
34
|
+
await update;
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
ignoreUpdates.delete(target);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** The outer private store wins for nested migration staging projects. */
|
|
41
|
+
export function privateStateRoot(target) {
|
|
42
|
+
const parts = path.resolve(target).split(path.sep);
|
|
43
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
44
|
+
if (parts[i] === PRODERO_DIRECTORY && LOCAL_STATE_DIRECTORIES.some(name => name === parts[i + 1]))
|
|
45
|
+
return parts.slice(0, i + 2).join(path.sep);
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
export async function protectPrivatePath(target) {
|
|
50
|
+
const root = privateStateRoot(target);
|
|
51
|
+
if (!root)
|
|
52
|
+
return;
|
|
53
|
+
await assertSafePath(target);
|
|
54
|
+
let cursor = root;
|
|
55
|
+
for (const part of ["", ...path.relative(root, target).split(path.sep).filter(Boolean)]) {
|
|
56
|
+
if (part)
|
|
57
|
+
cursor = path.join(cursor, part);
|
|
58
|
+
await tightenPrivateMode(cursor);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Create only the requested path; never recursively chmod preexisting children. */
|
|
62
|
+
export async function ensureStorageDirectory(target, forcePrivate = false) {
|
|
63
|
+
await assertSafePath(target);
|
|
64
|
+
const privateRoot = privateStateRoot(target);
|
|
65
|
+
if (privateRoot)
|
|
66
|
+
await fs.mkdir(path.dirname(privateRoot), { recursive: true });
|
|
67
|
+
await fs.mkdir(target, { recursive: true, ...(privateRoot || forcePrivate ? { mode: PRIVATE_DIRECTORY_MODE } : {}) });
|
|
68
|
+
await assertSafePath(target);
|
|
69
|
+
await protectPrivatePath(target);
|
|
70
|
+
if (forcePrivate)
|
|
71
|
+
await tightenPrivateMode(target);
|
|
72
|
+
}
|
|
73
|
+
const privateIgnoreUpdates = new Map();
|
|
74
|
+
/** Preference writes must not repair shared integration. Private self-exclusion
|
|
75
|
+
* protects files even when root ignore rules are absent; custom rules are never edited.
|
|
76
|
+
*/
|
|
77
|
+
export async function ensureSelfIgnoredPrivateDirectory(root, directory) {
|
|
78
|
+
const key = projectDataPath(root, directory), pending = privateIgnoreUpdates.get(key);
|
|
79
|
+
if (pending)
|
|
80
|
+
return pending;
|
|
81
|
+
const failure = () => new Error('PRODERO_LOCAL_IGNORE_UNAVAILABLE: Private ignore protection is unavailable. Preserve the existing local state and restore a safe self-excluding .gitignore containing only * in the private directory before retrying; shared project ignore rules were not changed.');
|
|
82
|
+
const update = (async () => {
|
|
83
|
+
try {
|
|
84
|
+
const target = await checkedDataPath(root, directory);
|
|
85
|
+
const ignore = await checkedDataPath(root, directory, '.gitignore');
|
|
86
|
+
const read = () => fs.readFile(ignore, 'utf8').catch(error => { if (error.code === 'ENOENT')
|
|
87
|
+
return null; throw error; });
|
|
88
|
+
const validate = (text) => {
|
|
89
|
+
const rules = text.split(/\r?\n/).filter(line => line !== '' && !line.startsWith('#'));
|
|
90
|
+
if (rules.length !== 1 || rules[0] !== '*')
|
|
91
|
+
throw failure();
|
|
92
|
+
};
|
|
93
|
+
const readReady = async () => {
|
|
94
|
+
let current = await read();
|
|
95
|
+
// Another process may have opened the exclusive marker but not written its
|
|
96
|
+
// one rule yet. Wait briefly; never overwrite an existing empty/custom file.
|
|
97
|
+
for (let attempt = 0; current === '' && attempt < 3; attempt++) {
|
|
98
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
99
|
+
current = await read();
|
|
100
|
+
}
|
|
101
|
+
return current;
|
|
102
|
+
};
|
|
103
|
+
const current = await readReady();
|
|
104
|
+
if (current !== null)
|
|
105
|
+
validate(current);
|
|
106
|
+
else {
|
|
107
|
+
await ensureStorageDirectory(target);
|
|
108
|
+
await writeFileExclusive(ignore, '*\n').catch(error => { if (error.code !== 'EEXIST')
|
|
109
|
+
throw error; });
|
|
110
|
+
validate((await readReady()) ?? '');
|
|
111
|
+
}
|
|
112
|
+
return target;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
throw failure();
|
|
116
|
+
}
|
|
117
|
+
})();
|
|
118
|
+
privateIgnoreUpdates.set(key, update);
|
|
119
|
+
try {
|
|
120
|
+
return await update;
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
privateIgnoreUpdates.delete(key);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
export async function writeFileExclusive(target, content) {
|
|
127
|
+
await assertSafePath(target);
|
|
128
|
+
await protectPrivatePath(target);
|
|
129
|
+
const privateFile = Boolean(privateStateRoot(target));
|
|
130
|
+
const handle = await fs.open(target, "wx", privateFile ? PRIVATE_FILE_MODE : 0o666);
|
|
131
|
+
try {
|
|
132
|
+
if (privateFile && process.platform !== "win32")
|
|
133
|
+
await handle.chmod(PRIVATE_FILE_MODE);
|
|
134
|
+
await handle.writeFile(content);
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
await handle.close();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/** Copies are new Curator-owned artifacts, so their entire destination tree may be secured. */
|
|
141
|
+
export async function copyStorageTree(source, target, forcePrivate = false, sharedMode) {
|
|
142
|
+
await assertSafePath(source);
|
|
143
|
+
await assertSafePath(target);
|
|
144
|
+
const stat = await fs.lstat(source);
|
|
145
|
+
const savedMode = !forcePrivate && !privateStateRoot(target) ? sharedMode?.(target, stat.mode & 0o777) : undefined;
|
|
146
|
+
if (stat.isDirectory()) {
|
|
147
|
+
await ensureStorageDirectory(target, forcePrivate);
|
|
148
|
+
if (savedMode !== undefined && process.platform !== "win32")
|
|
149
|
+
await fs.chmod(target, savedMode);
|
|
150
|
+
for (const name of await fs.readdir(source))
|
|
151
|
+
await copyStorageTree(path.join(source, name), path.join(target, name), forcePrivate, sharedMode);
|
|
152
|
+
}
|
|
153
|
+
else if (stat.isFile()) {
|
|
154
|
+
const privateFile = forcePrivate || Boolean(privateStateRoot(target));
|
|
155
|
+
// Open the destination restrictively BEFORE copying any bytes (copyFile inherits source mode).
|
|
156
|
+
const handle = await fs.open(target, "wx", privateFile ? PRIVATE_FILE_MODE : savedMode ?? 0o666);
|
|
157
|
+
try {
|
|
158
|
+
if (process.platform !== "win32" && (privateFile || savedMode !== undefined))
|
|
159
|
+
await handle.chmod(privateFile ? PRIVATE_FILE_MODE : savedMode);
|
|
160
|
+
const input = await fs.open(source, "r");
|
|
161
|
+
try {
|
|
162
|
+
for await (const chunk of input.createReadStream({ autoClose: false }))
|
|
163
|
+
await handle.writeFile(chunk);
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
await input.close();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
await handle.close();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
else
|
|
174
|
+
throw new Error("Unsupported migration/private copy source");
|
|
175
|
+
}
|
|
176
|
+
export async function exists(file) {
|
|
177
|
+
try {
|
|
178
|
+
await fs.access(file);
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
if (error.code !== "ENOENT")
|
|
183
|
+
throw error;
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
export function projectDataPath(root, ...parts) {
|
|
188
|
+
const base = path.resolve(root, PRODERO_DIRECTORY);
|
|
189
|
+
const target = path.resolve(base, ...parts);
|
|
190
|
+
if (target !== base && !target.startsWith(`${base}${path.sep}`))
|
|
191
|
+
throw new Error("Unsafe Prodero storage path");
|
|
192
|
+
return target;
|
|
193
|
+
}
|
|
194
|
+
/** Reject links in every existing component, including dangling links. Never follow them on writes. */
|
|
195
|
+
export async function assertSafePath(target) {
|
|
196
|
+
const absolute = path.resolve(target);
|
|
197
|
+
// Resolve OS aliases such as /tmp first; the storage boundary itself must never be an alias.
|
|
198
|
+
const marker = `${path.sep}${PRODERO_DIRECTORY}`;
|
|
199
|
+
const index = absolute.indexOf(`${marker}${path.sep}`);
|
|
200
|
+
const boundary = index >= 0 ? index : absolute.endsWith(marker) ? absolute.length - marker.length : -1;
|
|
201
|
+
const parent = boundary >= 0 ? absolute.slice(0, boundary) : path.dirname(absolute);
|
|
202
|
+
let cursor = await fs.realpath(parent);
|
|
203
|
+
const parts = boundary >= 0 ? absolute.slice(boundary + 1).split(path.sep) : [path.basename(absolute)];
|
|
204
|
+
for (const [offset, part] of parts.entries()) {
|
|
205
|
+
cursor = path.join(cursor, part);
|
|
206
|
+
try {
|
|
207
|
+
const stat = await fs.lstat(cursor);
|
|
208
|
+
if (stat.isSymbolicLink())
|
|
209
|
+
throw new Error(`Symlink in Prodero storage path: ${cursor}`);
|
|
210
|
+
if (offset < parts.length - 1 && !stat.isDirectory())
|
|
211
|
+
throw new Error(`Invalid Prodero storage directory: ${cursor}`);
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
if (error.code !== "ENOENT")
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/** Storage-level barrier also protects direct domain/plugin calls and recovery writes.
|
|
220
|
+
* Inspect only the version discriminator, never interpret unknown canonical content.
|
|
221
|
+
* Malformed config is handled by readConfig or interrupted-migration recovery.
|
|
222
|
+
*/
|
|
223
|
+
export async function assertCompatibleProject(root) {
|
|
224
|
+
const target = projectDataPath(root, "config.json");
|
|
225
|
+
await assertSafePath(target);
|
|
226
|
+
let raw;
|
|
227
|
+
try {
|
|
228
|
+
raw = JSON.parse(await fs.readFile(target, "utf8"));
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
if (error instanceof SyntaxError || error.code === "ENOENT")
|
|
232
|
+
return;
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
const version = raw && typeof raw === "object" && "schemaVersion" in raw ? raw.schemaVersion : undefined;
|
|
236
|
+
assertSupportedSchema(version);
|
|
237
|
+
assertSupportedKnowledgeSchema(raw && typeof raw === "object" && "knowledgeSchemaVersion" in raw ? raw.knowledgeSchemaVersion : undefined);
|
|
238
|
+
}
|
|
239
|
+
export function assertSupportedSchema(version) {
|
|
240
|
+
if (typeof version === "number" && version > PRODERO_SCHEMA_VERSION) {
|
|
241
|
+
throw new Error(`PRODERO_SCHEMA_NEWER: Project schema v${version} is newer than Prodero runtime schema v${PRODERO_SCHEMA_VERSION}. Update Prodero before reading or changing this project.`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
export function assertSupportedKnowledgeSchema(version) {
|
|
245
|
+
if (version !== undefined && (!Number.isInteger(version) || Number(version) < 1))
|
|
246
|
+
throw new Error("PRODERO_INVALID_CONFIG: Invalid knowledge schema version.");
|
|
247
|
+
if (typeof version === "number" && version > PRODERO_KNOWLEDGE_SCHEMA_VERSION)
|
|
248
|
+
throw new Error(`PRODERO_KNOWLEDGE_SCHEMA_NEWER: Knowledge format v${version} requires a newer Curator. Update Prodero before reading or changing knowledge.`);
|
|
249
|
+
}
|
|
250
|
+
export async function checkedDataPath(root, ...parts) {
|
|
251
|
+
await assertCompatibleProject(root);
|
|
252
|
+
const target = projectDataPath(root, ...parts);
|
|
253
|
+
await assertSafePath(target);
|
|
254
|
+
await protectPrivatePath(target);
|
|
255
|
+
return target;
|
|
256
|
+
}
|
|
257
|
+
export async function ensureDataDirectory(root, relative) {
|
|
258
|
+
const target = await checkedDataPath(root, relative);
|
|
259
|
+
const directory = path.relative(projectDataPath(root), target).split(path.sep)[0];
|
|
260
|
+
if (LOCAL_STATE_DIRECTORIES.some(local => local === directory))
|
|
261
|
+
await ensureLocalStateIgnored(root);
|
|
262
|
+
await ensureStorageDirectory(target);
|
|
263
|
+
return target;
|
|
264
|
+
}
|
|
265
|
+
export const CANONICAL_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
266
|
+
export function canonicalId(value, filename) {
|
|
267
|
+
if (typeof value !== "string" || !CANONICAL_ID_PATTERN.test(value))
|
|
268
|
+
throw new Error(`Invalid canonical ID: ${filename}`);
|
|
269
|
+
}
|
|
270
|
+
const positive = z.number().int().positive();
|
|
271
|
+
const configSchema = z.object({
|
|
272
|
+
schemaVersion: positive, knowledgeSchemaVersion: positive.optional(), initializedWith: z.string().min(1), lastUpdatedWith: z.string().min(1),
|
|
273
|
+
projectId: z.string().min(1), createdAt: z.string().refine(v => Number.isFinite(Date.parse(v))),
|
|
274
|
+
context: z.object({ max_tokens: positive.optional(), max_items: positive.optional(), required_max_tokens: z.number().int().nonnegative().optional(), budgetTokens: positive.optional(), requiredBudgetTokens: z.number().int().nonnegative().optional() }).passthrough().optional(),
|
|
275
|
+
presentation: presentationConfigSchema.optional(),
|
|
276
|
+
knowledgeEngine: z.object({ mode: z.enum(["deterministic", "assisted"]) }).passthrough().optional(),
|
|
277
|
+
}).passthrough();
|
|
278
|
+
export function validateConfig(value) {
|
|
279
|
+
const result = configSchema.safeParse(value);
|
|
280
|
+
if (!result.success)
|
|
281
|
+
throw new Error(`Invalid Prodero config: .prodero/config.json (${result.error.issues.map(i => i.path.join(".")).join(", ")}).`);
|
|
282
|
+
}
|
|
283
|
+
/** Ensure only the disposable working-tree-local derived-state root. */
|
|
284
|
+
export async function ensureDerivedStateRoot(root) {
|
|
285
|
+
return ensureDataDirectory(root, ".cache");
|
|
286
|
+
}
|
|
287
|
+
export async function ensureProjectDirectories(root) {
|
|
288
|
+
await Promise.all([
|
|
289
|
+
ensureDerivedStateRoot(root),
|
|
290
|
+
...PROJECT_DIRECTORIES.filter((directory) => directory !== ".cache").map((directory) => ensureDataDirectory(root, directory)),
|
|
291
|
+
]);
|
|
292
|
+
await Promise.all(PROJECT_DIRECTORIES.filter((directory) => directory !== ".cache")
|
|
293
|
+
.map(async (directory) => { const target = await checkedDataPath(root, directory, ".gitkeep"); if (!(await exists(target)))
|
|
294
|
+
await writeFileExclusive(target, "").catch(error => { if (error.code !== "EEXIST")
|
|
295
|
+
throw error; }); }));
|
|
296
|
+
}
|
|
297
|
+
/** Missing ignored directories are normal after clone. Do not touch tracked placeholders. */
|
|
298
|
+
export async function ensureRuntimeDirectories(root) {
|
|
299
|
+
for (const directory of ["local/memory", "local/skills", "sessions", "checkpoints", ".cache"])
|
|
300
|
+
await ensureDataDirectory(root, directory);
|
|
301
|
+
// Protect dormant legacy stores at their boundary without walking their contents.
|
|
302
|
+
for (const directory of LOCAL_STATE_DIRECTORIES)
|
|
303
|
+
await protectPrivatePath(projectDataPath(root, directory));
|
|
304
|
+
}
|
|
305
|
+
export async function countContentFiles(directory) {
|
|
306
|
+
if (!(await exists(directory)))
|
|
307
|
+
return 0;
|
|
308
|
+
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
309
|
+
return entries.filter((entry) => entry.isFile() && !entry.name.startsWith(".")).length;
|
|
310
|
+
}
|
|
311
|
+
export async function readConfig(root) {
|
|
312
|
+
const configPath = await checkedDataPath(root, "config.json");
|
|
313
|
+
if (!(await exists(configPath)))
|
|
314
|
+
return null;
|
|
315
|
+
let config;
|
|
316
|
+
try {
|
|
317
|
+
config = JSON.parse(await fs.readFile(configPath, "utf8"));
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
if (error instanceof SyntaxError)
|
|
321
|
+
throw new Error("PRODERO_INVALID_CONFIG: Invalid Prodero config: .prodero/config.json is not valid JSON. Restore valid configuration or use init to recover an interrupted migration.");
|
|
322
|
+
throw error;
|
|
323
|
+
}
|
|
324
|
+
validateConfig(config);
|
|
325
|
+
return config;
|
|
326
|
+
}
|
|
327
|
+
export async function writeConfig(root, config) {
|
|
328
|
+
validateConfig(config);
|
|
329
|
+
assertSupportedSchema(config.schemaVersion);
|
|
330
|
+
assertSupportedKnowledgeSchema(config.knowledgeSchemaVersion);
|
|
331
|
+
await writeFileAtomic(await checkedDataPath(root, "config.json"), `${JSON.stringify(config, null, 2)}\n`);
|
|
332
|
+
}
|
|
333
|
+
export async function writeFileAtomic(target, content) {
|
|
334
|
+
await assertSafePath(target);
|
|
335
|
+
await protectPrivatePath(target);
|
|
336
|
+
const temporary = `${target}.tmp-${process.pid}-${crypto.randomUUID()}`;
|
|
337
|
+
try {
|
|
338
|
+
await writeFileExclusive(temporary, content);
|
|
339
|
+
await fs.rename(temporary, target);
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
await fs.rm(temporary, { force: true }).catch(() => { });
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const checkpointSchema = z.object({
|
|
347
|
+
id: z.string().regex(/^[a-zA-Z0-9_-]+$/), createdAt: z.string().refine(v => Number.isFinite(Date.parse(v))),
|
|
348
|
+
sessions: z.array(z.object({ id: z.string().nullable().optional(), path: z.string().regex(/^[a-zA-Z0-9_-]+\.md$/), sha256: z.string().regex(/^[a-f0-9]{64}$/) })),
|
|
349
|
+
previousCheckpoint: z.string().nullable().optional(),
|
|
350
|
+
consolidation: z.enum(["not-performed", "deterministic-v1", "assisted-v1"]).optional(),
|
|
351
|
+
summary: z.object({ sessions: z.number().nonnegative(), candidates: z.number().nonnegative(), created: z.number().nonnegative(), consolidated: z.number().nonnegative(), duplicates: z.number().nonnegative(), superseded: z.number().nonnegative(), conflicts: z.number().nonnegative() }).optional(),
|
|
352
|
+
lifecycle: z.object({ operationId: z.string(), planId: z.string().regex(/^[a-f0-9]{64}$/), proposalHash: z.string().regex(/^[a-f0-9]{64}$/), transformationPreviewId: z.string().regex(/^[a-f0-9]{64}$/).optional() }).strict().optional(),
|
|
353
|
+
promotionRevisions: z.array(z.object({ path: z.string(), hash: z.string().regex(/^[a-f0-9]{64}$/) }).strict()).optional(),
|
|
354
|
+
observationHashes: z.array(z.string()).optional(),
|
|
355
|
+
createdMemory: z.array(z.string()).optional(), updatedMemory: z.array(z.string()).optional(), supersededMemory: z.array(z.string()).optional(),
|
|
356
|
+
duplicateMemory: z.array(z.string()).optional(), createdSkills: z.array(z.string()).optional(), conflicts: z.array(z.string()).optional(),
|
|
357
|
+
}).passthrough();
|
|
358
|
+
export function parseCheckpointData(content, filename) {
|
|
359
|
+
let raw;
|
|
360
|
+
try {
|
|
361
|
+
raw = JSON.parse(content);
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
throw new Error(`Invalid Prodero checkpoint: ${filename} is not valid JSON.`);
|
|
365
|
+
}
|
|
366
|
+
const result = checkpointSchema.safeParse(raw);
|
|
367
|
+
if (!result.success || (filename.startsWith("chk_") && result.data.id !== path.basename(filename, ".json")))
|
|
368
|
+
throw new Error(`Invalid Prodero checkpoint: ${filename} has malformed metadata.`);
|
|
369
|
+
return result.data;
|
|
370
|
+
}
|
|
371
|
+
export async function listCheckpoints(root) {
|
|
372
|
+
const directory = await checkedDataPath(root, "checkpoints");
|
|
373
|
+
if (!(await exists(directory)))
|
|
374
|
+
return [];
|
|
375
|
+
const names = (await fs.readdir(directory)).filter((name) => name.endsWith(".json")).sort();
|
|
376
|
+
const checkpoints = [];
|
|
377
|
+
for (const name of names) {
|
|
378
|
+
const raw = parseCheckpointData(await fs.readFile(await checkedDataPath(root, "checkpoints", name), "utf8"), name);
|
|
379
|
+
checkpoints.push({
|
|
380
|
+
id: raw.id,
|
|
381
|
+
createdAt: raw.createdAt,
|
|
382
|
+
previousCheckpoint: raw.previousCheckpoint ?? null,
|
|
383
|
+
sessions: raw.sessions.map((session) => ({ id: session.id ?? null, path: session.path, sha256: session.sha256 })),
|
|
384
|
+
consolidation: raw.consolidation ?? "not-performed",
|
|
385
|
+
createdMemory: raw.createdMemory ?? [],
|
|
386
|
+
createdSkills: raw.createdSkills ?? [],
|
|
387
|
+
updatedMemory: raw.updatedMemory ?? [],
|
|
388
|
+
supersededMemory: raw.supersededMemory ?? [],
|
|
389
|
+
duplicateMemory: raw.duplicateMemory ?? [],
|
|
390
|
+
conflicts: raw.conflicts ?? [],
|
|
391
|
+
summary: raw.summary ?? { sessions: raw.sessions.length, candidates: 0, created: 0, consolidated: 0, duplicates: 0, superseded: 0, conflicts: 0 },
|
|
392
|
+
note: raw.note,
|
|
393
|
+
importReceipt: raw.importReceipt,
|
|
394
|
+
curation: raw.curation,
|
|
395
|
+
curationDefer: raw.curationDefer,
|
|
396
|
+
lifecycle: raw.lifecycle,
|
|
397
|
+
promotionRevisions: raw.promotionRevisions,
|
|
398
|
+
intelligence: raw.intelligence, reasoning: raw.reasoning, instructionCandidates: raw.instructionCandidates, observationHashes: raw.observationHashes, knowledgeBefore: raw.knowledgeBefore,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
return checkpoints;
|
|
402
|
+
}
|
|
403
|
+
export function sha256(content) {
|
|
404
|
+
return crypto.createHash("sha256").update(content).digest("hex");
|
|
405
|
+
}
|
|
406
|
+
/** Block ordinary readers while a recoverable lifecycle transaction has partial files. */
|
|
407
|
+
/** Optimistic read fence: a lifecycle commit during a multi-file read must be retried.
|
|
408
|
+
* The journal blocks partial writes; the immutable checkpoint set detects a commit
|
|
409
|
+
* that started AND finished between the two journal checks. No persistent state.
|
|
410
|
+
*/
|
|
411
|
+
export async function knowledgeReadFence(root) {
|
|
412
|
+
const generation = async () => {
|
|
413
|
+
const directory = await checkedDataPath(root, 'checkpoints');
|
|
414
|
+
const checkpoints = (await fs.readdir(directory).catch(error => {
|
|
415
|
+
if (error.code === 'ENOENT')
|
|
416
|
+
return [];
|
|
417
|
+
throw error;
|
|
418
|
+
})).filter(name => /^chk_.*\.json$/.test(name)).sort();
|
|
419
|
+
// Migration does not necessarily create checkpoints. History revisions fence
|
|
420
|
+
// reads spanning an entire commit or rollback, including journal removal.
|
|
421
|
+
const history = await checkedDataPath(root, ".migrations");
|
|
422
|
+
const names = await fs.readdir(history).catch(error => {
|
|
423
|
+
if (error.code === "ENOENT")
|
|
424
|
+
return [];
|
|
425
|
+
throw error;
|
|
426
|
+
});
|
|
427
|
+
const revisions = await Promise.all(names.filter(name => name.endsWith(".json")).sort().map(async (name) => {
|
|
428
|
+
const entry = await checkedDataPath(root, ".migrations", name);
|
|
429
|
+
const stat = await fs.stat(entry);
|
|
430
|
+
return [name, stat.mtimeMs, stat.ctimeMs, stat.size];
|
|
431
|
+
}));
|
|
432
|
+
return JSON.stringify({ checkpoints, migrations: revisions });
|
|
433
|
+
};
|
|
434
|
+
const before = await generation();
|
|
435
|
+
await assertLifecycleReadable(root);
|
|
436
|
+
return async () => {
|
|
437
|
+
await assertLifecycleReadable(root);
|
|
438
|
+
if (await generation() !== before)
|
|
439
|
+
throw new Error('Knowledge transaction committed during read; retry against a stable graph');
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
export async function assertLifecycleReadable(root) {
|
|
443
|
+
const migration = await checkedDataPath(root, ".transactions/migration.json");
|
|
444
|
+
if (await exists(migration)) {
|
|
445
|
+
const { ownsSyncLock } = await import("./sync-transaction.js");
|
|
446
|
+
if (!ownsSyncLock(root))
|
|
447
|
+
throw new Error("Project update is in progress or requires recovery; open Prodero before reading knowledge.");
|
|
448
|
+
}
|
|
449
|
+
const journal = await checkedDataPath(root, ".transactions/pending.json");
|
|
450
|
+
const text = await fs.readFile(journal, "utf8").catch(error => { if (error.code === "ENOENT")
|
|
451
|
+
return null; throw error; });
|
|
452
|
+
let version;
|
|
453
|
+
try {
|
|
454
|
+
version = text ? JSON.parse(text).version : undefined;
|
|
455
|
+
}
|
|
456
|
+
catch {
|
|
457
|
+
throw new Error("PRODERO_INVALID_JOURNAL: invalid pending journal JSON; preserve it for recovery.");
|
|
458
|
+
}
|
|
459
|
+
if (version === 2)
|
|
460
|
+
throw new Error("Knowledge lifecycle transaction is pending recovery; run prepare or Sync before reading knowledge.");
|
|
461
|
+
}
|
|
462
|
+
//# sourceMappingURL=storage.js.map
|