@really-knows-ai/foundry 2.3.1 → 3.0.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/README.md +200 -198
- package/dist/.opencode/plugins/foundry-tools/appraiser-tools.js +28 -0
- package/dist/.opencode/plugins/foundry-tools/artefact-tools.js +58 -0
- package/dist/.opencode/plugins/foundry-tools/assay-tools.js +92 -0
- package/dist/.opencode/plugins/foundry-tools/attestation-tools.js +191 -0
- package/dist/.opencode/plugins/foundry-tools/config-create-tools.js +128 -0
- package/dist/.opencode/plugins/foundry-tools/config-law-tools.js +380 -0
- package/dist/.opencode/plugins/foundry-tools/config-tools.js +43 -0
- package/dist/.opencode/plugins/foundry-tools/feedback-tools.js +234 -0
- package/dist/.opencode/plugins/foundry-tools/git-helpers.js +354 -0
- package/dist/.opencode/plugins/foundry-tools/git-tools.js +181 -0
- package/dist/.opencode/plugins/foundry-tools/helpers.js +340 -0
- package/dist/.opencode/plugins/foundry-tools/history-tools.js +20 -0
- package/dist/.opencode/plugins/foundry-tools/memory-admin-tools.js +296 -0
- package/dist/.opencode/plugins/foundry-tools/memory-helpers.js +104 -0
- package/dist/.opencode/plugins/foundry-tools/memory-tools.js +286 -0
- package/dist/.opencode/plugins/foundry-tools/orchestrate-tool.js +159 -0
- package/dist/.opencode/plugins/foundry-tools/snapshot-tools.js +104 -0
- package/dist/.opencode/plugins/foundry-tools/stage-tools.js +186 -0
- package/dist/.opencode/plugins/foundry-tools/validate-tools.js +263 -0
- package/dist/.opencode/plugins/foundry-tools/workfile-tools.js +102 -0
- package/dist/.opencode/plugins/foundry.js +105 -0
- package/dist/CHANGELOG.md +490 -0
- package/dist/LICENSE +21 -0
- package/dist/README.md +278 -0
- package/dist/docs/README.md +59 -0
- package/dist/docs/architecture.md +434 -0
- package/dist/docs/concepts.md +396 -0
- package/dist/docs/getting-started.md +345 -0
- package/dist/docs/memory-maintenance.md +176 -0
- package/dist/docs/tools.md +1411 -0
- package/dist/docs/work-spec.md +283 -0
- package/dist/scripts/lib/artefacts.js +151 -0
- package/dist/scripts/lib/assay/loader.js +151 -0
- package/dist/scripts/lib/assay/parse-jsonl.js +102 -0
- package/dist/scripts/lib/assay/permissions.js +52 -0
- package/dist/scripts/lib/assay/run.js +219 -0
- package/dist/scripts/lib/assay/spawn-with-timeout.js +138 -0
- package/dist/scripts/lib/attestation/attest.js +111 -0
- package/dist/scripts/lib/attestation/canonical-json.js +109 -0
- package/dist/scripts/lib/attestation/hash.js +17 -0
- package/dist/scripts/lib/attestation/parse.js +14 -0
- package/dist/scripts/lib/attestation/payload.js +106 -0
- package/dist/scripts/lib/attestation/render.js +16 -0
- package/dist/scripts/lib/attestation/verify.js +15 -0
- package/dist/scripts/lib/branch-guard.js +72 -0
- package/dist/scripts/lib/config-creators/appraiser.js +9 -0
- package/dist/scripts/lib/config-creators/artefact-type.js +9 -0
- package/dist/scripts/lib/config-creators/cycle.js +11 -0
- package/dist/scripts/lib/config-creators/factory.js +49 -0
- package/dist/scripts/lib/config-creators/flow.js +11 -0
- package/dist/scripts/lib/config-validators/appraiser.js +49 -0
- package/dist/scripts/lib/config-validators/artefact-type.js +38 -0
- package/dist/scripts/lib/config-validators/cycle.js +131 -0
- package/dist/scripts/lib/config-validators/flow.js +57 -0
- package/dist/scripts/lib/config-validators/helpers.js +96 -0
- package/dist/scripts/lib/config-validators/law.js +96 -0
- package/dist/scripts/lib/config.js +393 -0
- package/dist/scripts/lib/failed-flow.js +131 -0
- package/dist/scripts/lib/feedback-store.js +249 -0
- package/dist/scripts/lib/feedback-transitions.js +105 -0
- package/dist/scripts/lib/finalize.js +70 -0
- package/dist/scripts/lib/foundational-guards.js +13 -0
- package/dist/scripts/lib/git-bridge.js +77 -0
- package/dist/scripts/lib/git-finish/work-finish.js +233 -0
- package/dist/scripts/lib/git-policy.js +101 -0
- package/dist/scripts/lib/guards.js +125 -0
- package/dist/scripts/lib/history.js +132 -0
- package/dist/scripts/lib/memory/admin/create-edge-type.js +91 -0
- package/dist/scripts/lib/memory/admin/create-entity-type.js +43 -0
- package/dist/scripts/lib/memory/admin/create-extractor.js +67 -0
- package/dist/scripts/lib/memory/admin/drop-edge-type.js +40 -0
- package/dist/scripts/lib/memory/admin/drop-entity-type.js +172 -0
- package/dist/scripts/lib/memory/admin/dump.js +47 -0
- package/dist/scripts/lib/memory/admin/helpers.js +31 -0
- package/dist/scripts/lib/memory/admin/init.js +170 -0
- package/dist/scripts/lib/memory/admin/live-store.js +76 -0
- package/dist/scripts/lib/memory/admin/reembed.js +285 -0
- package/dist/scripts/lib/memory/admin/rename-edge-type.js +54 -0
- package/dist/scripts/lib/memory/admin/rename-entity-type.js +151 -0
- package/dist/scripts/lib/memory/admin/reset.js +24 -0
- package/dist/scripts/lib/memory/admin/vacuum.js +9 -0
- package/dist/scripts/lib/memory/admin/validate.js +19 -0
- package/dist/scripts/lib/memory/config.js +149 -0
- package/dist/scripts/lib/memory/cozo.js +136 -0
- package/dist/scripts/lib/memory/drift.js +71 -0
- package/dist/scripts/lib/memory/embeddings.js +128 -0
- package/dist/scripts/lib/memory/frontmatter.js +75 -0
- package/dist/scripts/lib/memory/ndjson.js +84 -0
- package/dist/scripts/lib/memory/paths.js +25 -0
- package/dist/scripts/lib/memory/permissions.js +41 -0
- package/dist/scripts/lib/memory/prompt.js +109 -0
- package/dist/scripts/lib/memory/query.js +56 -0
- package/dist/scripts/lib/memory/reads.js +109 -0
- package/dist/scripts/lib/memory/schema.js +64 -0
- package/dist/scripts/lib/memory/search.js +73 -0
- package/dist/scripts/lib/memory/singleton.js +49 -0
- package/dist/scripts/lib/memory/store.js +162 -0
- package/dist/scripts/lib/memory/types.js +93 -0
- package/dist/scripts/lib/memory/validate.js +58 -0
- package/dist/scripts/lib/memory/writes.js +40 -0
- package/{scripts → dist/scripts}/lib/pending.js +7 -2
- package/dist/scripts/lib/secret.js +59 -0
- package/{scripts → dist/scripts}/lib/slug.js +3 -2
- package/dist/scripts/lib/snapshot/finish.js +103 -0
- package/dist/scripts/lib/snapshot/inspect.js +253 -0
- package/dist/scripts/lib/snapshot/render.js +55 -0
- package/dist/scripts/lib/sort-fs-check.js +121 -0
- package/dist/scripts/lib/sort-routing.js +101 -0
- package/{scripts → dist/scripts}/lib/stage-guard.js +12 -6
- package/{scripts → dist/scripts}/lib/state.js +4 -0
- package/dist/scripts/lib/token.js +57 -0
- package/dist/scripts/lib/tracing.js +59 -0
- package/dist/scripts/lib/ulid.js +100 -0
- package/dist/scripts/lib/validator-jsonl.js +162 -0
- package/{scripts → dist/scripts}/lib/workfile.js +38 -20
- package/dist/scripts/orchestrate-cycle.js +215 -0
- package/dist/scripts/orchestrate-phases.js +314 -0
- package/dist/scripts/orchestrate.js +163 -0
- package/dist/scripts/sort.js +278 -0
- package/{skills → dist/skills}/add-appraiser/SKILL.md +42 -6
- package/{skills → dist/skills}/add-artefact-type/SKILL.md +49 -21
- package/{skills → dist/skills}/add-cycle/SKILL.md +60 -14
- package/dist/skills/add-extractor/SKILL.md +133 -0
- package/{skills → dist/skills}/add-flow/SKILL.md +39 -7
- package/dist/skills/add-law/SKILL.md +191 -0
- package/dist/skills/add-memory-edge-type/SKILL.md +52 -0
- package/dist/skills/add-memory-entity-type/SKILL.md +74 -0
- package/{skills → dist/skills}/appraise/SKILL.md +62 -13
- package/dist/skills/assay/SKILL.md +72 -0
- package/dist/skills/change-embedding-model/SKILL.md +58 -0
- package/dist/skills/drop-memory-edge-type/SKILL.md +54 -0
- package/dist/skills/drop-memory-entity-type/SKILL.md +57 -0
- package/dist/skills/dry-run/SKILL.md +116 -0
- package/{skills → dist/skills}/flow/SKILL.md +15 -2
- package/dist/skills/forge/SKILL.md +121 -0
- package/dist/skills/human-appraise/SKILL.md +153 -0
- package/{skills → dist/skills}/init-foundry/SKILL.md +23 -4
- package/dist/skills/init-memory/SKILL.md +92 -0
- package/{skills → dist/skills}/orchestrate/SKILL.md +30 -4
- package/dist/skills/quench/SKILL.md +99 -0
- package/{skills → dist/skills}/refresh-agents/SKILL.md +1 -1
- package/dist/skills/rename-memory-edge-type/SKILL.md +50 -0
- package/dist/skills/rename-memory-entity-type/SKILL.md +51 -0
- package/dist/skills/reset-memory/SKILL.md +54 -0
- package/dist/skills/upgrade-foundry/SKILL.md +192 -0
- package/package.json +34 -17
- package/.opencode/plugins/foundry.js +0 -761
- package/CHANGELOG.md +0 -90
- package/docs/concepts.md +0 -59
- package/docs/getting-started.md +0 -78
- package/docs/work-spec.md +0 -193
- package/scripts/lib/artefacts.js +0 -124
- package/scripts/lib/config.js +0 -175
- package/scripts/lib/feedback-transitions.js +0 -25
- package/scripts/lib/feedback.js +0 -440
- package/scripts/lib/finalize.js +0 -41
- package/scripts/lib/history.js +0 -59
- package/scripts/lib/secret.js +0 -23
- package/scripts/lib/tags.js +0 -108
- package/scripts/lib/token.js +0 -26
- package/scripts/orchestrate.js +0 -418
- package/scripts/sort.js +0 -370
- package/scripts/validate-tags.js +0 -54
- package/skills/add-law/SKILL.md +0 -105
- package/skills/forge/SKILL.md +0 -88
- package/skills/human-appraise/SKILL.md +0 -82
- package/skills/quench/SKILL.md +0 -62
- package/skills/upgrade-foundry/SKILL.md +0 -216
- /package/{skills → dist/skills}/list-agents/SKILL.md +0 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { openStore, closeStore, syncStore } from '../store.js';
|
|
2
|
+
import { loadSchema, writeSchema, bumpVersion } from '../schema.js';
|
|
3
|
+
import { entRelName } from '../cozo.js';
|
|
4
|
+
import { putEntity } from '../writes.js';
|
|
5
|
+
import { invalidateStore } from '../singleton.js';
|
|
6
|
+
|
|
7
|
+
function assertEmbedder(embedder) {
|
|
8
|
+
if (!embedder) throw new Error('reembed requires an embedder');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function assertDimensions(newDimensions) {
|
|
12
|
+
if (!Number.isInteger(newDimensions) || newDimensions <= 0) {
|
|
13
|
+
throw new Error('newDimensions must be positive integer');
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function assertDbPath(dbAbsolutePath) {
|
|
18
|
+
if (!dbAbsolutePath) throw new Error('reembed requires dbAbsolutePath');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function assertRawIO(rawIO) {
|
|
22
|
+
if (!rawIO) throw new Error('reembed requires rawIO for absolute path operations');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function validateReembedParams(opts) {
|
|
26
|
+
assertEmbedder(opts.embedder);
|
|
27
|
+
assertDimensions(opts.newDimensions);
|
|
28
|
+
assertDbPath(opts.dbAbsolutePath);
|
|
29
|
+
assertRawIO(opts.rawIO);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function assertVectorLength(v, expected) {
|
|
33
|
+
const len = Array.isArray(v) ? v.length : 'n/a';
|
|
34
|
+
if (!Array.isArray(v) || v.length !== expected) {
|
|
35
|
+
throw new Error(`reembed: vector length ${len} != expected ${expected}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function harvestRows(foundryDir, schema, io, dbPath) {
|
|
40
|
+
const entityTypes = Object.keys(schema.entities);
|
|
41
|
+
const store = await openStore({
|
|
42
|
+
foundryDir, schema, io, dbAbsolutePath: dbPath,
|
|
43
|
+
});
|
|
44
|
+
const rowsByType = {};
|
|
45
|
+
try {
|
|
46
|
+
for (const type of entityTypes) {
|
|
47
|
+
const rel = entRelName(type);
|
|
48
|
+
const res = await store.db.run(
|
|
49
|
+
`?[name, value] := *${rel}{name, value}`,
|
|
50
|
+
);
|
|
51
|
+
rowsByType[type] = res.rows.map(([name, value]) => ({ name, value }));
|
|
52
|
+
}
|
|
53
|
+
} finally {
|
|
54
|
+
closeStore(store);
|
|
55
|
+
}
|
|
56
|
+
return { rowsByType, entityTypes };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function embedChunk(store, chunk, vectors, dims, vocab) {
|
|
60
|
+
for (let j = 0; j < chunk.length; j++) {
|
|
61
|
+
assertVectorLength(vectors[j], dims);
|
|
62
|
+
const entry = {
|
|
63
|
+
type: chunk[j].type,
|
|
64
|
+
name: chunk[j].name,
|
|
65
|
+
value: chunk[j].value,
|
|
66
|
+
};
|
|
67
|
+
await putEntity(store, entry, vocab, {
|
|
68
|
+
embedder: async () => [vectors[j]],
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function processBatch(opts) {
|
|
74
|
+
const chunk = opts.rows
|
|
75
|
+
.slice(opts.start, opts.start + opts.batchSize)
|
|
76
|
+
.map((r) => ({ ...r, type: opts.type }));
|
|
77
|
+
const vectors = await opts.embedder(chunk.map((r) => r.value));
|
|
78
|
+
await embedChunk(
|
|
79
|
+
opts.store, chunk, vectors, opts.dims, opts.vocab,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function embedType(store, type, rows, opts) {
|
|
84
|
+
for (let i = 0; i < rows.length; i += opts.batchSize) {
|
|
85
|
+
await processBatch({
|
|
86
|
+
store, rows, start: i,
|
|
87
|
+
batchSize: opts.batchSize, type,
|
|
88
|
+
embedder: opts.embedder,
|
|
89
|
+
dims: opts.newDimensions,
|
|
90
|
+
vocab: opts.vocab,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function buildStagingDb(opts) {
|
|
96
|
+
const vocab = {
|
|
97
|
+
entities: opts.newSchema.entities,
|
|
98
|
+
edges: opts.newSchema.edges,
|
|
99
|
+
};
|
|
100
|
+
const stagingStore = await openStore({
|
|
101
|
+
foundryDir: opts.foundryDir,
|
|
102
|
+
schema: opts.newSchema,
|
|
103
|
+
io: opts.io,
|
|
104
|
+
dbAbsolutePath: opts.stagingPath,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const embedOpts = {
|
|
108
|
+
batchSize: opts.batchSize,
|
|
109
|
+
embedder: opts.embedder,
|
|
110
|
+
newDimensions: opts.newDimensions,
|
|
111
|
+
vocab,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
for (const type of opts.entityTypes) {
|
|
116
|
+
const rows = opts.rowsByType[type] ?? [];
|
|
117
|
+
await embedType(stagingStore, type, rows, embedOpts);
|
|
118
|
+
}
|
|
119
|
+
} catch (err) {
|
|
120
|
+
try { closeStore(stagingStore); } catch { /* best effort */ }
|
|
121
|
+
throw err;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
closeStore(stagingStore);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function swapDatabases(opts) {
|
|
128
|
+
await writeSchema(opts.foundryDir, opts.newSchema, opts.io);
|
|
129
|
+
renameDbFiles(opts.stagingPath, opts.dbAbsolutePath, opts.rawIO);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function refreshStore(opts) {
|
|
133
|
+
const reopened = await openStore({
|
|
134
|
+
foundryDir: opts.foundryDir,
|
|
135
|
+
schema: opts.newSchema,
|
|
136
|
+
io: opts.io,
|
|
137
|
+
dbAbsolutePath: opts.dbAbsolutePath,
|
|
138
|
+
});
|
|
139
|
+
try {
|
|
140
|
+
await syncStore({ store: reopened, io: opts.io });
|
|
141
|
+
} finally {
|
|
142
|
+
closeStore(reopened);
|
|
143
|
+
}
|
|
144
|
+
invalidateStore(opts.worktreeRoot);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function runPipeline(opts) {
|
|
148
|
+
const buildOpts = {
|
|
149
|
+
stagingPath: opts.stagingPath,
|
|
150
|
+
newSchema: opts.newSchema,
|
|
151
|
+
io: opts.io,
|
|
152
|
+
foundryDir: opts.foundryDir,
|
|
153
|
+
entityTypes: opts.entityTypes,
|
|
154
|
+
rowsByType: opts.rowsByType,
|
|
155
|
+
embedder: opts.embedder,
|
|
156
|
+
batchSize: opts.batchSize,
|
|
157
|
+
newDimensions: opts.newDimensions,
|
|
158
|
+
};
|
|
159
|
+
await buildStagingDb(buildOpts);
|
|
160
|
+
|
|
161
|
+
const swapOpts = {
|
|
162
|
+
stagingPath: opts.stagingPath,
|
|
163
|
+
dbAbsolutePath: opts.dbAbsolutePath,
|
|
164
|
+
foundryDir: opts.foundryDir,
|
|
165
|
+
newSchema: opts.newSchema,
|
|
166
|
+
io: opts.io,
|
|
167
|
+
rawIO: opts.rawIO,
|
|
168
|
+
};
|
|
169
|
+
await swapDatabases(swapOpts);
|
|
170
|
+
|
|
171
|
+
await refreshStore({
|
|
172
|
+
foundryDir: opts.foundryDir,
|
|
173
|
+
newSchema: opts.newSchema,
|
|
174
|
+
io: opts.io,
|
|
175
|
+
dbAbsolutePath: opts.dbAbsolutePath,
|
|
176
|
+
worktreeRoot: opts.worktreeRoot,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function withCleanup(stagingPath, rawIO, fn) {
|
|
181
|
+
return fn().catch((err) => {
|
|
182
|
+
unlinkDbFiles(stagingPath, rawIO);
|
|
183
|
+
throw err;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Atomic re-embedding via a staging DB.
|
|
189
|
+
*
|
|
190
|
+
* Builds the new state in a sibling staging DB while the original `memory.db`
|
|
191
|
+
* and on-disk schema remain untouched. After every entity is re-embedded
|
|
192
|
+
* successfully, we swap atomically: rename the staging DB over the live DB,
|
|
193
|
+
* write the new schema, and refresh NDJSON.
|
|
194
|
+
*
|
|
195
|
+
* On any failure — provider error, unexpected vector length, Cozo error —
|
|
196
|
+
* the staging DB is closed and unlinked and the original state is preserved.
|
|
197
|
+
*/
|
|
198
|
+
export async function reembed({
|
|
199
|
+
worktreeRoot,
|
|
200
|
+
io,
|
|
201
|
+
rawIO,
|
|
202
|
+
dbAbsolutePath,
|
|
203
|
+
newModel,
|
|
204
|
+
newDimensions,
|
|
205
|
+
embedder,
|
|
206
|
+
batchSize = 64,
|
|
207
|
+
}) {
|
|
208
|
+
validateReembedParams({
|
|
209
|
+
embedder, newDimensions, dbAbsolutePath, rawIO,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const foundryDir = 'foundry';
|
|
213
|
+
const oldSchema = await loadSchema(foundryDir, io);
|
|
214
|
+
const { rowsByType, entityTypes } = await harvestRows(
|
|
215
|
+
foundryDir, oldSchema, io, dbAbsolutePath,
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const stagingPath = `${dbAbsolutePath}.reembed-tmp`;
|
|
219
|
+
unlinkDbFiles(stagingPath, rawIO);
|
|
220
|
+
|
|
221
|
+
const newSchema = {
|
|
222
|
+
...oldSchema,
|
|
223
|
+
embeddings: { model: newModel, dimensions: newDimensions },
|
|
224
|
+
};
|
|
225
|
+
bumpVersion(newSchema);
|
|
226
|
+
|
|
227
|
+
await withCleanup(stagingPath, rawIO, () => runPipeline({
|
|
228
|
+
stagingPath, newSchema, io, foundryDir,
|
|
229
|
+
entityTypes, rowsByType, embedder, batchSize,
|
|
230
|
+
newDimensions, dbAbsolutePath, rawIO, worktreeRoot,
|
|
231
|
+
}));
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
model: newModel,
|
|
235
|
+
dimensions: newDimensions,
|
|
236
|
+
types: entityTypes.length,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Remove a Cozo sqlite DB file and its WAL/SHM sidecars if present.
|
|
242
|
+
* Operates on absolute filesystem paths (reembed works outside the IO
|
|
243
|
+
* shim's foundry-relative tree).
|
|
244
|
+
*/
|
|
245
|
+
function unlinkDbFiles(dbPath, rawIO) {
|
|
246
|
+
for (const suffix of ['', '-wal', '-shm']) {
|
|
247
|
+
const p = dbPath + suffix;
|
|
248
|
+
try {
|
|
249
|
+
if (rawIO.exists(p)) rawIO.unlink(p);
|
|
250
|
+
} catch {
|
|
251
|
+
// best-effort cleanup
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function tryUnlinkIfExists(rawIO, path) {
|
|
257
|
+
if (rawIO.exists(path)) {
|
|
258
|
+
try { rawIO.unlink(path); } catch { /* ignore */ }
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function tryRenameIfExists(rawIO, src, dest) {
|
|
263
|
+
if (rawIO.exists(src)) {
|
|
264
|
+
try {
|
|
265
|
+
rawIO.rename(src, dest);
|
|
266
|
+
} catch {
|
|
267
|
+
try { rawIO.unlink(src); } catch { /* ignore */ }
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Atomically move a Cozo sqlite DB plus its WAL/SHM sidecars into place.
|
|
274
|
+
*
|
|
275
|
+
* Sqlite treats the main DB file as the authoritative name; WAL/SHM files
|
|
276
|
+
* are recreated on next open. We still move them when present so that a
|
|
277
|
+
* subsequent open picks up any pending state.
|
|
278
|
+
*/
|
|
279
|
+
function renameDbFiles(fromPath, toPath, rawIO) {
|
|
280
|
+
tryUnlinkIfExists(rawIO, toPath + '-wal');
|
|
281
|
+
tryUnlinkIfExists(rawIO, toPath + '-shm');
|
|
282
|
+
rawIO.rename(fromPath, toPath);
|
|
283
|
+
tryRenameIfExists(rawIO, fromPath + '-wal', toPath + '-wal');
|
|
284
|
+
tryRenameIfExists(rawIO, fromPath + '-shm', toPath + '-shm');
|
|
285
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
|
|
2
|
+
import { memoryPaths } from '../paths.js';
|
|
3
|
+
import { loadSchema, writeSchema, bumpVersion, hashFrontmatter } from '../schema.js';
|
|
4
|
+
import { invalidateStore } from '../singleton.js';
|
|
5
|
+
import { parseFrontmatter } from '../frontmatter.js';
|
|
6
|
+
import { renderEdgeFrontmatter, composeMarkdown } from './helpers.js';
|
|
7
|
+
|
|
8
|
+
const IDENT = /^[a-z][a-z0-9_]*$/;
|
|
9
|
+
|
|
10
|
+
function validateIdentifiers(from, to) {
|
|
11
|
+
if (!IDENT.test(to)) throw new Error(`invalid identifier: '${to}'`);
|
|
12
|
+
if (from === to) throw new Error(`from and to identical`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function validateSchemaState(schema, from, to) {
|
|
16
|
+
if (!schema.edges[from]) throw new Error(`edge type '${from}' not declared`);
|
|
17
|
+
if (schema.edges[to] || schema.entities[to]) throw new Error(`'${to}' already exists`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function renameEdgeType({ worktreeRoot, io, from, to }) {
|
|
21
|
+
validateIdentifiers(from, to);
|
|
22
|
+
|
|
23
|
+
const foundryDir = 'foundry';
|
|
24
|
+
const p = memoryPaths(foundryDir);
|
|
25
|
+
const schema = await loadSchema(foundryDir, io);
|
|
26
|
+
validateSchemaState(schema, from, to);
|
|
27
|
+
|
|
28
|
+
const oldFile = p.edgeTypeFile(from);
|
|
29
|
+
const text = await io.readFile(oldFile);
|
|
30
|
+
const parsed = parseFrontmatter(text, { filename: oldFile });
|
|
31
|
+
if (!parsed.hasFrontmatter) throw new Error(`edge type file lacks frontmatter`);
|
|
32
|
+
const fm = parsed.frontmatter;
|
|
33
|
+
fm.type = to;
|
|
34
|
+
const body = parsed.body;
|
|
35
|
+
await io.writeFile(p.edgeTypeFile(to), composeMarkdown(renderEdgeFrontmatter(fm), body));
|
|
36
|
+
await io.unlink(oldFile);
|
|
37
|
+
|
|
38
|
+
const oldRel = p.relationFile(from);
|
|
39
|
+
if (await io.exists(oldRel)) {
|
|
40
|
+
const rel = await io.readFile(oldRel);
|
|
41
|
+
await io.writeFile(p.relationFile(to), rel);
|
|
42
|
+
await io.unlink(oldRel);
|
|
43
|
+
} else {
|
|
44
|
+
await io.writeFile(p.relationFile(to), '');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
schema.edges[to] = { frontmatterHash: hashFrontmatter({ type: to, sources: fm.sources, targets: fm.targets }) };
|
|
48
|
+
delete schema.edges[from];
|
|
49
|
+
bumpVersion(schema);
|
|
50
|
+
await writeSchema(foundryDir, schema, io);
|
|
51
|
+
|
|
52
|
+
invalidateStore(worktreeRoot);
|
|
53
|
+
return { from, to };
|
|
54
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
|
|
2
|
+
import { memoryPaths } from '../paths.js';
|
|
3
|
+
import { loadSchema, writeSchema, bumpVersion, hashFrontmatter } from '../schema.js';
|
|
4
|
+
import { parseEdgeRows, serialiseEdgeRows, parseEntityRows, serialiseEntityRows } from '../ndjson.js';
|
|
5
|
+
import { invalidateStore } from '../singleton.js';
|
|
6
|
+
import { parseFrontmatter } from '../frontmatter.js';
|
|
7
|
+
import { renderEdgeFrontmatter, composeMarkdown } from './helpers.js';
|
|
8
|
+
|
|
9
|
+
const IDENT = /^[a-z][a-z0-9_]*$/;
|
|
10
|
+
const TYPE_LINE = /^type:\s*\S.*$/m;
|
|
11
|
+
const FRONTMATTER_BLOCK = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
12
|
+
|
|
13
|
+
function assertValidIdentifier(id) {
|
|
14
|
+
if (!IDENT.test(id)) throw new Error(`invalid identifier: '${id}'`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function assertDistinct(from, to) {
|
|
18
|
+
if (from === to) throw new Error(`from and to are identical`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function assertSourceExists(schema, from) {
|
|
22
|
+
if (!schema.entities[from]) throw new Error(`entity type '${from}' not declared`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function assertTargetFree(schema, to) {
|
|
26
|
+
if (schema.entities[to]) throw new Error(`'${to}' already exists`);
|
|
27
|
+
if (schema.edges[to]) throw new Error(`'${to}' already exists`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function validateRename(schema, from, to) {
|
|
31
|
+
assertValidIdentifier(to);
|
|
32
|
+
assertDistinct(from, to);
|
|
33
|
+
assertSourceExists(schema, from);
|
|
34
|
+
assertTargetFree(schema, to);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function rewriteEntityTypeFile(from, to, p, io) {
|
|
38
|
+
const oldFile = p.entityTypeFile(from);
|
|
39
|
+
const text = await io.readFile(oldFile);
|
|
40
|
+
const newText = text.replace(FRONTMATTER_BLOCK, (_, fm) => {
|
|
41
|
+
const replaced = fm.replace(TYPE_LINE, `type: ${to}`);
|
|
42
|
+
return `---\n${replaced}\n---`;
|
|
43
|
+
});
|
|
44
|
+
await io.writeFile(p.entityTypeFile(to), newText);
|
|
45
|
+
await io.unlink(oldFile);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function rewriteEntityRelationFile(from, to, p, io) {
|
|
49
|
+
const oldRel = p.relationFile(from);
|
|
50
|
+
if (await io.exists(oldRel)) {
|
|
51
|
+
const rows = parseEntityRows(await io.readFile(oldRel));
|
|
52
|
+
await io.writeFile(p.relationFile(to), serialiseEntityRows(rows));
|
|
53
|
+
await io.unlink(oldRel);
|
|
54
|
+
} else {
|
|
55
|
+
await io.writeFile(p.relationFile(to), '');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function mapEdgeArray(arr, from, to) {
|
|
60
|
+
return arr.map((x) => (x === from ? to : x));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function needsRename(fm, from) {
|
|
64
|
+
if (fm === 'any') return false;
|
|
65
|
+
return Array.isArray(fm) && fm.includes(from);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function applyRename(fm, from, to) {
|
|
69
|
+
let changed = false;
|
|
70
|
+
for (const key of ['sources', 'targets']) {
|
|
71
|
+
if (needsRename(fm[key], from)) {
|
|
72
|
+
fm[key] = mapEdgeArray(fm[key], from, to);
|
|
73
|
+
changed = true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return changed;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function mapEdgeRows(rows, from, to) {
|
|
80
|
+
let rowsChanged = false;
|
|
81
|
+
const newRows = rows.map((r) => {
|
|
82
|
+
let nr = r;
|
|
83
|
+
if (r.from_type === from) { nr = { ...nr, from_type: to }; rowsChanged = true; }
|
|
84
|
+
if (r.to_type === from) { nr = { ...nr, to_type: to }; rowsChanged = true; }
|
|
85
|
+
return nr;
|
|
86
|
+
});
|
|
87
|
+
return { newRows, rowsChanged };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function updateEdgeTypeRows(edgeName, from, to, ctx) {
|
|
91
|
+
const relFile = ctx.p.relationFile(edgeName);
|
|
92
|
+
if (await ctx.io.exists(relFile)) {
|
|
93
|
+
const rows = parseEdgeRows(await ctx.io.readFile(relFile));
|
|
94
|
+
const { newRows, rowsChanged } = mapEdgeRows(rows, from, to);
|
|
95
|
+
if (rowsChanged) await ctx.io.writeFile(relFile, serialiseEdgeRows(newRows));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function updateEdgeTypeFrontmatter(edgeName, from, to, ctx) {
|
|
100
|
+
const edgeFile = ctx.p.edgeTypeFile(edgeName);
|
|
101
|
+
const edgeText = await ctx.io.readFile(edgeFile);
|
|
102
|
+
const parsed = parseFrontmatter(edgeText, { filename: edgeFile });
|
|
103
|
+
if (!parsed.hasFrontmatter) return null;
|
|
104
|
+
|
|
105
|
+
const changed = applyRename(parsed.frontmatter, from, to);
|
|
106
|
+
if (!changed) return null;
|
|
107
|
+
|
|
108
|
+
const body = parsed.body;
|
|
109
|
+
const hash = hashFrontmatter({
|
|
110
|
+
type: edgeName,
|
|
111
|
+
sources: parsed.frontmatter.sources,
|
|
112
|
+
targets: parsed.frontmatter.targets,
|
|
113
|
+
});
|
|
114
|
+
await ctx.io.writeFile(edgeFile, composeMarkdown(renderEdgeFrontmatter(parsed.frontmatter), body));
|
|
115
|
+
return hash;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function updateAllEdgeTypes(from, to, ctx) {
|
|
119
|
+
const hashes = new Map();
|
|
120
|
+
for (const edgeName of Object.keys(ctx.schema.edges)) {
|
|
121
|
+
const hash = await updateEdgeTypeFrontmatter(edgeName, from, to, ctx);
|
|
122
|
+
if (hash !== null) hashes.set(edgeName, hash);
|
|
123
|
+
await updateEdgeTypeRows(edgeName, from, to, ctx);
|
|
124
|
+
}
|
|
125
|
+
for (const [edgeName, hash] of hashes) {
|
|
126
|
+
ctx.schema.edges[edgeName].frontmatterHash = hash;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function updateSchema(schema, from, to, foundryDir, io) {
|
|
131
|
+
schema.entities[to] = { frontmatterHash: hashFrontmatter({ type: to }) };
|
|
132
|
+
delete schema.entities[from];
|
|
133
|
+
bumpVersion(schema);
|
|
134
|
+
await writeSchema(foundryDir, schema, io);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function renameEntityType({ worktreeRoot, io, from, to }) {
|
|
138
|
+
const foundryDir = 'foundry';
|
|
139
|
+
const p = memoryPaths(foundryDir);
|
|
140
|
+
const schema = await loadSchema(foundryDir, io);
|
|
141
|
+
|
|
142
|
+
validateRename(schema, from, to);
|
|
143
|
+
await rewriteEntityTypeFile(from, to, p, io);
|
|
144
|
+
await rewriteEntityRelationFile(from, to, p, io);
|
|
145
|
+
const ctx = { p, io, schema };
|
|
146
|
+
await updateAllEdgeTypes(from, to, ctx);
|
|
147
|
+
await updateSchema(schema, from, to, foundryDir, io);
|
|
148
|
+
|
|
149
|
+
invalidateStore(worktreeRoot);
|
|
150
|
+
return { from, to };
|
|
151
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
|
|
2
|
+
import { memoryPaths } from '../paths.js';
|
|
3
|
+
import { loadSchema, writeSchema, bumpVersion } from '../schema.js';
|
|
4
|
+
import { invalidateStore } from '../singleton.js';
|
|
5
|
+
|
|
6
|
+
export async function resetMemory({ worktreeRoot, io, confirm }) {
|
|
7
|
+
if (confirm !== true) throw new Error(`reset requires confirm: true`);
|
|
8
|
+
const foundryDir = 'foundry';
|
|
9
|
+
const p = memoryPaths(foundryDir);
|
|
10
|
+
const schema = await loadSchema(foundryDir, io);
|
|
11
|
+
|
|
12
|
+
for (const name of [...Object.keys(schema.entities), ...Object.keys(schema.edges)]) {
|
|
13
|
+
await io.writeFile(p.relationFile(name), '');
|
|
14
|
+
}
|
|
15
|
+
// Delete the live DB so it's re-imported empty on next open.
|
|
16
|
+
for (const suffix of ['', '-wal', '-shm']) {
|
|
17
|
+
if (await io.exists(p.db + suffix)) await io.unlink(p.db + suffix);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
bumpVersion(schema);
|
|
21
|
+
await writeSchema(foundryDir, schema, io);
|
|
22
|
+
invalidateStore(worktreeRoot);
|
|
23
|
+
return { reset: true };
|
|
24
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { loadMemoryConfig } from '../config.js';
|
|
2
|
+
import { loadSchema } from '../schema.js';
|
|
3
|
+
import { loadVocabulary } from '../types.js';
|
|
4
|
+
import { detectDrift } from '../drift.js';
|
|
5
|
+
|
|
6
|
+
export async function validateMemory({ io }) {
|
|
7
|
+
const issues = [];
|
|
8
|
+
try {
|
|
9
|
+
const config = await loadMemoryConfig('foundry', io);
|
|
10
|
+
if (!config.present) issues.push({ kind: 'missing-config', message: 'foundry/memory/config.md missing' });
|
|
11
|
+
const schema = await loadSchema('foundry', io);
|
|
12
|
+
const vocab = await loadVocabulary('foundry', io);
|
|
13
|
+
const drift = detectDrift({ vocabulary: vocab, schema });
|
|
14
|
+
for (const item of drift.items) issues.push(item);
|
|
15
|
+
} catch (err) {
|
|
16
|
+
issues.push({ kind: 'load-error', message: err.message });
|
|
17
|
+
}
|
|
18
|
+
return { ok: issues.length === 0, issues };
|
|
19
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { memoryPaths } from './paths.js';
|
|
2
|
+
import { parseFrontmatter, renderMarkdown } from './frontmatter.js';
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_CONFIG = Object.freeze({
|
|
5
|
+
enabled: false,
|
|
6
|
+
present: false,
|
|
7
|
+
validation: 'strict',
|
|
8
|
+
embeddings: Object.freeze({
|
|
9
|
+
enabled: true,
|
|
10
|
+
baseURL: 'http://localhost:11434/v1',
|
|
11
|
+
model: 'nomic-embed-text',
|
|
12
|
+
dimensions: 768,
|
|
13
|
+
apiKey: null,
|
|
14
|
+
batchSize: 64,
|
|
15
|
+
timeoutMs: 30000,
|
|
16
|
+
}),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function mergeEmbeddingsKey(base, key, value) {
|
|
20
|
+
if (key in base && value !== undefined) base[key] = value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function mergeEmbeddings(userE) {
|
|
24
|
+
const base = { ...DEFAULT_CONFIG.embeddings };
|
|
25
|
+
if (!userE || typeof userE !== 'object') return base;
|
|
26
|
+
for (const [key, value] of Object.entries(userE)) {
|
|
27
|
+
mergeEmbeddingsKey(base, key, value);
|
|
28
|
+
}
|
|
29
|
+
return base;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function assertValidationValid(value) {
|
|
33
|
+
if (!['strict', 'lax'].includes(value)) {
|
|
34
|
+
throw new Error(`memory config: validation must be 'strict' or 'lax', got ${JSON.stringify(value)}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function assertEmbeddingsEnabled(e) {
|
|
39
|
+
if (typeof e.enabled !== 'boolean') throw new Error('memory config: embeddings.enabled must be boolean');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function assertValidBaseUrl(e) {
|
|
43
|
+
if (!(typeof e.baseURL === 'string' && e.baseURL)) throw new Error('memory config: embeddings.baseURL required');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function assertValidModel(e) {
|
|
47
|
+
if (!(typeof e.model === 'string' && e.model)) throw new Error('memory config: embeddings.model required');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function assertValidDimensions(e) {
|
|
51
|
+
if (!(Number.isInteger(e.dimensions) && e.dimensions > 0)) throw new Error('memory config: embeddings.dimensions must be positive integer');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function assertValidBatchSize(e) {
|
|
55
|
+
if (!(Number.isInteger(e.batchSize) && e.batchSize > 0)) throw new Error('memory config: embeddings.batchSize must be positive integer');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function assertEmbeddingsFields(e) {
|
|
59
|
+
assertValidBaseUrl(e);
|
|
60
|
+
assertValidModel(e);
|
|
61
|
+
assertValidDimensions(e);
|
|
62
|
+
assertValidBatchSize(e);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function validate(cfg) {
|
|
66
|
+
assertValidationValid(cfg.validation);
|
|
67
|
+
assertEmbeddingsEnabled(cfg.embeddings);
|
|
68
|
+
if (cfg.embeddings.enabled) {
|
|
69
|
+
assertEmbeddingsFields(cfg.embeddings);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function assertEnabledValid(enabled, configPath) {
|
|
74
|
+
if (enabled !== undefined && typeof enabled !== 'boolean') {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`memory config (${configPath}): enabled must be a YAML boolean (true/false), got ${JSON.stringify(enabled)}`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function buildConfig(fm) {
|
|
82
|
+
const cfg = {
|
|
83
|
+
present: true,
|
|
84
|
+
enabled: fm.enabled === true,
|
|
85
|
+
validation: fm.validation ?? DEFAULT_CONFIG.validation,
|
|
86
|
+
embeddings: mergeEmbeddings(fm.embeddings),
|
|
87
|
+
};
|
|
88
|
+
if (!cfg.enabled) {
|
|
89
|
+
cfg.embeddings = { ...cfg.embeddings, enabled: false };
|
|
90
|
+
}
|
|
91
|
+
return cfg;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function loadMemoryConfig(foundryDir, io) {
|
|
95
|
+
const p = memoryPaths(foundryDir);
|
|
96
|
+
if (!(await io.exists(p.config))) {
|
|
97
|
+
return { ...DEFAULT_CONFIG, embeddings: { ...DEFAULT_CONFIG.embeddings } };
|
|
98
|
+
}
|
|
99
|
+
const text = await io.readFile(p.config);
|
|
100
|
+
const { frontmatter: fm } = parseFrontmatter(text, { filename: p.config });
|
|
101
|
+
assertEnabledValid(fm.enabled, p.config);
|
|
102
|
+
const cfg = buildConfig(fm);
|
|
103
|
+
validate(cfg);
|
|
104
|
+
return cfg;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function loadExistingFrontmatter(p, io) {
|
|
108
|
+
if (!(await io.exists(p.config))) {
|
|
109
|
+
return { frontmatter: {}, body: '' };
|
|
110
|
+
}
|
|
111
|
+
const text = await io.readFile(p.config);
|
|
112
|
+
const parsed = parseFrontmatter(text, { filename: p.config });
|
|
113
|
+
if (parsed.hasFrontmatter) {
|
|
114
|
+
return { frontmatter: parsed.frontmatter, body: parsed.body };
|
|
115
|
+
}
|
|
116
|
+
return { frontmatter: {}, body: text };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function mergeEmbeddingsUpdate(existingFm, updates) {
|
|
120
|
+
const baseE = typeof existingFm.embeddings === 'object' && existingFm.embeddings
|
|
121
|
+
? existingFm.embeddings
|
|
122
|
+
: {};
|
|
123
|
+
return { ...baseE, ...updates.embeddings };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function mergeFrontmatterUpdates(existingFm, updates) {
|
|
127
|
+
const nextFm = { ...existingFm };
|
|
128
|
+
if ('enabled' in updates) nextFm.enabled = updates.enabled;
|
|
129
|
+
if ('validation' in updates) nextFm.validation = updates.validation;
|
|
130
|
+
if (updates.embeddings && typeof updates.embeddings === 'object') {
|
|
131
|
+
nextFm.embeddings = mergeEmbeddingsUpdate(existingFm, updates);
|
|
132
|
+
}
|
|
133
|
+
return nextFm;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Rewrite foundry/memory/config.md with updated embeddings settings.
|
|
138
|
+
* Preserves any existing markdown body after the frontmatter. If config.md
|
|
139
|
+
* is missing, creates a minimal one with no body.
|
|
140
|
+
*
|
|
141
|
+
* `updates.embeddings` is merged into existing embeddings frontmatter; other
|
|
142
|
+
* top-level keys in `updates` (enabled, validation) overwrite if provided.
|
|
143
|
+
*/
|
|
144
|
+
export async function writeMemoryConfig(foundryDir, updates, io) {
|
|
145
|
+
const p = memoryPaths(foundryDir);
|
|
146
|
+
const { frontmatter: existingFm, body } = await loadExistingFrontmatter(p, io);
|
|
147
|
+
const nextFm = mergeFrontmatterUpdates(existingFm, updates);
|
|
148
|
+
await io.writeFile(p.config, renderMarkdown(nextFm, body));
|
|
149
|
+
}
|