@x12i/memorix-service 3.0.5 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/dist/ai-services/client.d.ts +31 -0
- package/dist/ai-services/client.d.ts.map +1 -0
- package/dist/ai-services/client.js +104 -0
- package/dist/ai-services/client.js.map +1 -0
- package/dist/ai-services/config.d.ts +17 -0
- package/dist/ai-services/config.d.ts.map +1 -0
- package/dist/ai-services/config.js +44 -0
- package/dist/ai-services/config.js.map +1 -0
- package/dist/ai-services/normalize-output.d.ts +23 -0
- package/dist/ai-services/normalize-output.d.ts.map +1 -0
- package/dist/ai-services/normalize-output.js +148 -0
- package/dist/ai-services/normalize-output.js.map +1 -0
- package/dist/ai-services/skill-execution.d.ts +23 -0
- package/dist/ai-services/skill-execution.d.ts.map +1 -0
- package/dist/ai-services/skill-execution.js +54 -0
- package/dist/ai-services/skill-execution.js.map +1 -0
- package/dist/ai-services/types.d.ts +62 -0
- package/dist/ai-services/types.d.ts.map +1 -0
- package/dist/ai-services/types.js +3 -0
- package/dist/ai-services/types.js.map +1 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +37 -6
- package/dist/app.js.map +1 -1
- package/dist/assist/ask.d.ts +35 -0
- package/dist/assist/ask.d.ts.map +1 -0
- package/dist/assist/ask.js +346 -0
- package/dist/assist/ask.js.map +1 -0
- package/dist/assist/facts.d.ts +5 -0
- package/dist/assist/facts.d.ts.map +1 -0
- package/dist/assist/facts.js +237 -0
- package/dist/assist/facts.js.map +1 -0
- package/dist/assist/merge-ask-proposal.d.ts +9 -0
- package/dist/assist/merge-ask-proposal.d.ts.map +1 -0
- package/dist/assist/merge-ask-proposal.js +57 -0
- package/dist/assist/merge-ask-proposal.js.map +1 -0
- package/dist/assist/types.d.ts +60 -0
- package/dist/assist/types.d.ts.map +1 -0
- package/dist/assist/types.js +57 -0
- package/dist/assist/types.js.map +1 -0
- package/dist/assist/validate.d.ts +4 -0
- package/dist/assist/validate.d.ts.map +1 -0
- package/dist/assist/validate.js +319 -0
- package/dist/assist/validate.js.map +1 -0
- package/dist/cli.js +29 -6
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/isolation.d.ts +1 -1
- package/dist/isolation.d.ts.map +1 -1
- package/dist/journey/collect.js +2 -2
- package/dist/journey/collect.js.map +1 -1
- package/dist/journey/coverage.js +3 -3
- package/dist/journey/coverage.js.map +1 -1
- package/dist/known-packs.d.ts +1 -1
- package/dist/known-packs.d.ts.map +1 -1
- package/dist/known-packs.js +1 -1
- package/dist/known-packs.js.map +1 -1
- package/dist/live.d.ts +2 -1
- package/dist/live.d.ts.map +1 -1
- package/dist/live.js +239 -2
- package/dist/live.js.map +1 -1
- package/dist/magit/magit-pack-service.d.ts +167 -0
- package/dist/magit/magit-pack-service.d.ts.map +1 -0
- package/dist/magit/magit-pack-service.js +699 -0
- package/dist/magit/magit-pack-service.js.map +1 -0
- package/dist/magit/pack-to-patchset.d.ts +45 -0
- package/dist/magit/pack-to-patchset.d.ts.map +1 -0
- package/dist/magit/pack-to-patchset.js +196 -0
- package/dist/magit/pack-to-patchset.js.map +1 -0
- package/dist/magit/package-catalog-bridge.d.ts +58 -0
- package/dist/magit/package-catalog-bridge.d.ts.map +1 -0
- package/dist/magit/package-catalog-bridge.js +220 -0
- package/dist/magit/package-catalog-bridge.js.map +1 -0
- package/dist/magit/proposal-to-pack.d.ts +22 -0
- package/dist/magit/proposal-to-pack.d.ts.map +1 -0
- package/dist/magit/proposal-to-pack.js +68 -0
- package/dist/magit/proposal-to-pack.js.map +1 -0
- package/dist/mongo-metadata-store.d.ts.map +1 -1
- package/dist/mongo-metadata-store.js +11 -3
- package/dist/mongo-metadata-store.js.map +1 -1
- package/dist/openapi/openapi.json +665 -1
- package/dist/org-sources.d.ts +14 -0
- package/dist/org-sources.d.ts.map +1 -0
- package/dist/org-sources.js +31 -0
- package/dist/org-sources.js.map +1 -0
- package/dist/pipeline-compile.d.ts +51 -0
- package/dist/pipeline-compile.d.ts.map +1 -0
- package/dist/pipeline-compile.js +203 -0
- package/dist/pipeline-compile.js.map +1 -0
- package/dist/platform.d.ts +22 -1
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +150 -21
- package/dist/platform.js.map +1 -1
- package/dist/routes/ai-services.d.ts +4 -0
- package/dist/routes/ai-services.d.ts.map +1 -0
- package/dist/routes/ai-services.js +239 -0
- package/dist/routes/ai-services.js.map +1 -0
- package/dist/routes/assist.d.ts +4 -0
- package/dist/routes/assist.d.ts.map +1 -0
- package/dist/routes/assist.js +75 -0
- package/dist/routes/assist.js.map +1 -0
- package/dist/routes/connector-invocations.js +2 -2
- package/dist/routes/data-journey.js +11 -11
- package/dist/routes/data-journey.js.map +1 -1
- package/dist/routes/intelligence.d.ts.map +1 -1
- package/dist/routes/intelligence.js +19 -1
- package/dist/routes/intelligence.js.map +1 -1
- package/dist/routes/memory.d.ts.map +1 -1
- package/dist/routes/memory.js +343 -41
- package/dist/routes/memory.js.map +1 -1
- package/dist/routes/metadata.d.ts.map +1 -1
- package/dist/routes/metadata.js +800 -8
- package/dist/routes/metadata.js.map +1 -1
- package/dist/routes/pipelines.d.ts.map +1 -1
- package/dist/routes/pipelines.js +28 -3
- package/dist/routes/pipelines.js.map +1 -1
- package/dist/routes/relationships.d.ts.map +1 -1
- package/dist/routes/relationships.js +29 -0
- package/dist/routes/relationships.js.map +1 -1
- package/dist/routes/simulation.d.ts +7 -0
- package/dist/routes/simulation.d.ts.map +1 -0
- package/dist/routes/simulation.js +22 -0
- package/dist/routes/simulation.js.map +1 -0
- package/dist/simulation.d.ts +26 -0
- package/dist/simulation.d.ts.map +1 -0
- package/dist/simulation.js +46 -0
- package/dist/simulation.js.map +1 -0
- package/openapi/openapi.json +665 -1
- package/package.json +19 -10
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MagitPackService — server-only façade over @x12i/magit for pack versioning.
|
|
3
|
+
* Phase 0–5: ensureRepo, saveVersion, history, diff, packAtCommit, publish, proposals.
|
|
4
|
+
*/
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { MongoClient } from "mongodb";
|
|
9
|
+
import { magit, listProposalLedgerEntries, isPatchsetError, createProgressReporter, labelDiff, } from "@x12i/magit";
|
|
10
|
+
import { checksumMetadataPack } from "@x12i/memorix-metadata-runtime";
|
|
11
|
+
import { ServiceError } from "../errors.js";
|
|
12
|
+
import { isOrgRuntimeFolderSource, stripOrgRuntimeSourcesFromPack, } from "../org-sources.js";
|
|
13
|
+
import { MAGIT_AGENTS_COLLECTION, MAGIT_SEED_AGENT_ID, packToPatchset, parseRecordId, parseSaveVersionIdempotencyKey, recordIdForDefinition, recordsToPack, } from "./pack-to-patchset.js";
|
|
14
|
+
import { mergeProposalIntoPack, } from "./proposal-to-pack.js";
|
|
15
|
+
const MEMORIX_LENS = {
|
|
16
|
+
[MAGIT_AGENTS_COLLECTION]: {
|
|
17
|
+
docKey: "agentId",
|
|
18
|
+
explode: {
|
|
19
|
+
path: "definitions",
|
|
20
|
+
recordId: "{agentId}/{kind}/{id}",
|
|
21
|
+
recordFields: ["kind", "id"],
|
|
22
|
+
carry: ["agentId"],
|
|
23
|
+
},
|
|
24
|
+
preserve: "passthrough",
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
const MAGITIGNORE = `# Memorix Phase 0 — ignore install ledger (not versioned)
|
|
28
|
+
memorix_metadata_install_ledger
|
|
29
|
+
memorix_metadata_releases
|
|
30
|
+
`;
|
|
31
|
+
const RELEASES_FILE = "memorix-releases.json";
|
|
32
|
+
/**
|
|
33
|
+
* Magit is on by default when durable Mongo is configured (platform gates that).
|
|
34
|
+
* Set METADATA_MAGIT=0 / false / off to disable.
|
|
35
|
+
*/
|
|
36
|
+
export function isMetadataMagitEnabled() {
|
|
37
|
+
const v = process.env.METADATA_MAGIT?.trim().toLowerCase();
|
|
38
|
+
if (v === "0" || v === "false" || v === "off" || v === "no")
|
|
39
|
+
return false;
|
|
40
|
+
// Default-on when unset or any other truthy-ish value (including "1"/"true"/"on").
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
export function resolveMagitRepoRootParent() {
|
|
44
|
+
const fromEnv = process.env.MAGIT_REPO_ROOT?.trim();
|
|
45
|
+
if (fromEnv)
|
|
46
|
+
return fromEnv;
|
|
47
|
+
return join(process.cwd(), ".tmp", "magit-repos");
|
|
48
|
+
}
|
|
49
|
+
function isLiveRef(ref) {
|
|
50
|
+
return ref === "live" || ref === "LIVE";
|
|
51
|
+
}
|
|
52
|
+
function isHeadRef(ref) {
|
|
53
|
+
return ref === "HEAD" || ref === "head";
|
|
54
|
+
}
|
|
55
|
+
function fieldDiffs(before, after) {
|
|
56
|
+
const beforeObj = before && typeof before === "object" && !Array.isArray(before)
|
|
57
|
+
? before
|
|
58
|
+
: null;
|
|
59
|
+
const afterObj = after && typeof after === "object" && !Array.isArray(after)
|
|
60
|
+
? after
|
|
61
|
+
: null;
|
|
62
|
+
if (!beforeObj && !afterObj)
|
|
63
|
+
return undefined;
|
|
64
|
+
const keys = new Set([
|
|
65
|
+
...Object.keys(beforeObj ?? {}),
|
|
66
|
+
...Object.keys(afterObj ?? {}),
|
|
67
|
+
]);
|
|
68
|
+
const fields = [];
|
|
69
|
+
for (const path of [...keys].sort()) {
|
|
70
|
+
const hasB = beforeObj != null && path in beforeObj;
|
|
71
|
+
const hasA = afterObj != null && path in afterObj;
|
|
72
|
+
if (!hasB && hasA)
|
|
73
|
+
fields.push({ path, change: "added" });
|
|
74
|
+
else if (hasB && !hasA)
|
|
75
|
+
fields.push({ path, change: "removed" });
|
|
76
|
+
else if (hasB &&
|
|
77
|
+
hasA &&
|
|
78
|
+
JSON.stringify(beforeObj[path]) !== JSON.stringify(afterObj[path])) {
|
|
79
|
+
fields.push({ path, change: "changed" });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return fields.length ? fields : undefined;
|
|
83
|
+
}
|
|
84
|
+
function unwrapDefinitionBody(body) {
|
|
85
|
+
if (!body || typeof body !== "object")
|
|
86
|
+
return body;
|
|
87
|
+
const rec = body;
|
|
88
|
+
if (rec.definition && typeof rec.definition === "object")
|
|
89
|
+
return rec.definition;
|
|
90
|
+
return body;
|
|
91
|
+
}
|
|
92
|
+
export class MagitPackService {
|
|
93
|
+
cfg;
|
|
94
|
+
ensured = new Set();
|
|
95
|
+
constructor(cfg) {
|
|
96
|
+
this.cfg = cfg;
|
|
97
|
+
}
|
|
98
|
+
progress() {
|
|
99
|
+
const log = this.cfg.log;
|
|
100
|
+
createProgressReporter({ mode: "json" });
|
|
101
|
+
return {
|
|
102
|
+
tick(phase, detail) {
|
|
103
|
+
log?.(`[magit] ${phase}`, detail);
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
repoRootFor(orgId) {
|
|
108
|
+
return join(this.cfg.repoRootParent, orgId);
|
|
109
|
+
}
|
|
110
|
+
async ensureRepo(orgId) {
|
|
111
|
+
if (this.ensured.has(orgId))
|
|
112
|
+
return;
|
|
113
|
+
const { tick } = this.progress();
|
|
114
|
+
try {
|
|
115
|
+
this.ensureMongoEnv();
|
|
116
|
+
mkdirSync(this.cfg.repoRootParent, { recursive: true });
|
|
117
|
+
const repoRoot = this.repoRootFor(orgId);
|
|
118
|
+
mkdirSync(repoRoot, { recursive: true });
|
|
119
|
+
const repo = await magit.open({ repoRoot });
|
|
120
|
+
const configPath = join(repoRoot, ".magit", "config.json");
|
|
121
|
+
if (!existsSync(configPath)) {
|
|
122
|
+
tick("ensureRepo.init", { orgId });
|
|
123
|
+
await repo.init({
|
|
124
|
+
name: `memorix-metadata-${orgId}`,
|
|
125
|
+
driver: "transform",
|
|
126
|
+
urlEnv: this.cfg.mongoUrlEnv,
|
|
127
|
+
db: this.cfg.metadataDb,
|
|
128
|
+
driverConfig: {
|
|
129
|
+
kind: "transform",
|
|
130
|
+
base: {
|
|
131
|
+
kind: "mongo",
|
|
132
|
+
urlEnv: this.cfg.mongoUrlEnv,
|
|
133
|
+
db: this.cfg.metadataDb,
|
|
134
|
+
},
|
|
135
|
+
lens: MEMORIX_LENS,
|
|
136
|
+
encodingProfile: "current",
|
|
137
|
+
},
|
|
138
|
+
storageProvider: "x12i-http",
|
|
139
|
+
storageUrl: this.cfg.storageUrl ||
|
|
140
|
+
process.env.MAGIT_STORAGE_URL?.trim() ||
|
|
141
|
+
"https://magit-storage.x12i.com",
|
|
142
|
+
storageTokenEnv: this.cfg.storageTokenEnv || "MAGIT_TOKEN",
|
|
143
|
+
storageTenant: orgId,
|
|
144
|
+
environmentName: "draft",
|
|
145
|
+
skipRemoteBootstrap: true,
|
|
146
|
+
});
|
|
147
|
+
writeFileSync(join(repoRoot, ".magitignore"), MAGITIGNORE, "utf8");
|
|
148
|
+
await this.seedAgentsCollectionCommit(repo);
|
|
149
|
+
}
|
|
150
|
+
this.ensured.add(orgId);
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
throw this.mapError(err, "failed to ensure Magit repo");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async saveVersion(orgId, pack, opts) {
|
|
157
|
+
const message = opts.message?.trim();
|
|
158
|
+
if (!message) {
|
|
159
|
+
throw new ServiceError("VALIDATION", "message required for save version", 400);
|
|
160
|
+
}
|
|
161
|
+
await this.ensureRepo(orgId);
|
|
162
|
+
const { tick } = this.progress();
|
|
163
|
+
try {
|
|
164
|
+
const cleanPack = stripOrgRuntimeSourcesFromPack(pack);
|
|
165
|
+
tick("saveVersion.start", { orgId, agentId: cleanPack.agentId });
|
|
166
|
+
const repo = await magit.open({ repoRoot: this.repoRootFor(orgId) });
|
|
167
|
+
const tip = (await repo.listCommitsFromHead({ maxCount: 1 }))[0]?.hash;
|
|
168
|
+
// Magit history is cumulative — delete prior org folder source records so
|
|
169
|
+
// packAtCommit cannot resurrect src_* after a stripped save.
|
|
170
|
+
const deleteRecordIds = [];
|
|
171
|
+
if (tip) {
|
|
172
|
+
try {
|
|
173
|
+
const tipPack = await this.packAtCommitRaw(orgId, cleanPack.agentId, tip);
|
|
174
|
+
for (const src of tipPack.pack.sources ?? []) {
|
|
175
|
+
if (!isOrgRuntimeFolderSource(src))
|
|
176
|
+
continue;
|
|
177
|
+
const id = String(src.id ?? "");
|
|
178
|
+
if (!id)
|
|
179
|
+
continue;
|
|
180
|
+
deleteRecordIds.push(recordIdForDefinition(cleanPack.agentId, "sources", id));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// Tip may be seed-only; proceed with puts only.
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const { patchset, checksum } = packToPatchset(cleanPack, {
|
|
188
|
+
message,
|
|
189
|
+
actor: opts.actor?.subject,
|
|
190
|
+
baseCommit: tip,
|
|
191
|
+
deleteRecordIds,
|
|
192
|
+
});
|
|
193
|
+
const result = await repo.proposePatchset(patchset);
|
|
194
|
+
// Magit proposals are content-addressed and do not move HEAD. Pin the tip so
|
|
195
|
+
// the next agent/pack save inherits prior records (multi-agent release sets).
|
|
196
|
+
if (!result.reused) {
|
|
197
|
+
this.pinPackTip(repo, result.commitHash);
|
|
198
|
+
}
|
|
199
|
+
tick("saveVersion.done", { commit: result.commitHash, reused: result.reused });
|
|
200
|
+
return {
|
|
201
|
+
commit: result.commitHash,
|
|
202
|
+
checksum,
|
|
203
|
+
reused: result.reused,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
throw this.mapError(err, "failed to save pack version");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Advance Magit HEAD / main to a proposal commit so subsequent proposePatchset
|
|
212
|
+
* calls with base HEAD inherit prior agent records.
|
|
213
|
+
*/
|
|
214
|
+
pinPackTip(repo, commitHash) {
|
|
215
|
+
const storeDir = repo.repoPaths().storeDir;
|
|
216
|
+
mkdirSync(join(storeDir, "refs", "heads"), { recursive: true });
|
|
217
|
+
writeFileSync(join(storeDir, "HEAD"), `${commitHash}\n`, "utf8");
|
|
218
|
+
writeFileSync(join(storeDir, "refs", "heads", "main"), `${commitHash}\n`, "utf8");
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Merge proposal draft into base pack and save a Magit version (not live).
|
|
222
|
+
*/
|
|
223
|
+
async importProposal(orgId, opts) {
|
|
224
|
+
const agentId = opts.agentId?.trim();
|
|
225
|
+
if (!agentId)
|
|
226
|
+
throw new ServiceError("VALIDATION", "agentId required", 400);
|
|
227
|
+
if (!opts.proposal?.proposalId?.trim()) {
|
|
228
|
+
throw new ServiceError("VALIDATION", "proposalId required", 400);
|
|
229
|
+
}
|
|
230
|
+
let pack;
|
|
231
|
+
try {
|
|
232
|
+
pack = mergeProposalIntoPack(opts.basePack, opts.proposal, agentId);
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
throw new ServiceError("VALIDATION", err instanceof Error ? err.message : String(err), 400);
|
|
236
|
+
}
|
|
237
|
+
const saved = await this.saveVersion(orgId, pack, {
|
|
238
|
+
message: `proposal:${opts.proposal.proposalId.trim()}`,
|
|
239
|
+
actor: opts.actor,
|
|
240
|
+
});
|
|
241
|
+
return { ...saved, pack };
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Snapshot a live pack as the initial Magit commit for legacy checksum-only installs.
|
|
245
|
+
*/
|
|
246
|
+
async backfillLineage(orgId, pack, opts) {
|
|
247
|
+
return this.saveVersion(orgId, pack, {
|
|
248
|
+
message: "backfill: initial snapshot",
|
|
249
|
+
actor: opts?.actor,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
async history(orgId, agentId, opts) {
|
|
253
|
+
if (!agentId?.trim()) {
|
|
254
|
+
throw new ServiceError("VALIDATION", "agentId required", 400);
|
|
255
|
+
}
|
|
256
|
+
await this.ensureRepo(orgId);
|
|
257
|
+
const maxCount = opts?.maxCount ?? 50;
|
|
258
|
+
try {
|
|
259
|
+
const repoRoot = this.repoRootFor(orgId);
|
|
260
|
+
const repo = await magit.open({ repoRoot });
|
|
261
|
+
const metagitDir = repo.repoPaths().metagitDir;
|
|
262
|
+
const proposals = await listProposalLedgerEntries(metagitDir);
|
|
263
|
+
const saved = [];
|
|
264
|
+
for (const entry of proposals) {
|
|
265
|
+
const parsed = parseSaveVersionIdempotencyKey(entry.idempotencyKey);
|
|
266
|
+
if (!parsed || parsed.agentId !== agentId)
|
|
267
|
+
continue;
|
|
268
|
+
saved.push({
|
|
269
|
+
commit: entry.commitHash,
|
|
270
|
+
agentId: parsed.agentId,
|
|
271
|
+
checksum: parsed.checksum,
|
|
272
|
+
message: null,
|
|
273
|
+
createdAt: entry.createdAt,
|
|
274
|
+
source: "save-version",
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
const headCommits = await repo.listCommitsFromHead({ maxCount });
|
|
278
|
+
const head = headCommits.map((c) => ({
|
|
279
|
+
commit: c.hash,
|
|
280
|
+
agentId,
|
|
281
|
+
checksum: typeof c.proposal?.provenance?.checksum === "string"
|
|
282
|
+
? c.proposal.provenance.checksum
|
|
283
|
+
: null,
|
|
284
|
+
message: c.message ?? null,
|
|
285
|
+
createdAt: c.timestamp,
|
|
286
|
+
source: "head",
|
|
287
|
+
}));
|
|
288
|
+
const byCommit = new Map();
|
|
289
|
+
for (const row of head)
|
|
290
|
+
byCommit.set(row.commit, row);
|
|
291
|
+
for (const row of saved)
|
|
292
|
+
byCommit.set(row.commit, row);
|
|
293
|
+
return [...byCommit.values()]
|
|
294
|
+
.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
|
295
|
+
.slice(0, maxCount);
|
|
296
|
+
}
|
|
297
|
+
catch (err) {
|
|
298
|
+
throw this.mapError(err, "failed to list pack versions");
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async diff(orgId, agentId, opts) {
|
|
302
|
+
if (!agentId?.trim()) {
|
|
303
|
+
throw new ServiceError("VALIDATION", "agentId required", 400);
|
|
304
|
+
}
|
|
305
|
+
const from = opts.from?.trim();
|
|
306
|
+
const to = opts.to?.trim();
|
|
307
|
+
if (!from || !to) {
|
|
308
|
+
throw new ServiceError("VALIDATION", "from and to required", 400);
|
|
309
|
+
}
|
|
310
|
+
await this.ensureRepo(orgId);
|
|
311
|
+
const { tick } = this.progress();
|
|
312
|
+
try {
|
|
313
|
+
tick("diff.start", { orgId, agentId, from, to });
|
|
314
|
+
this.ensureMongoEnv();
|
|
315
|
+
const repo = await magit.open({ repoRoot: this.repoRootFor(orgId) });
|
|
316
|
+
const changes = await this.collectDiffRows(repo, agentId, from, to);
|
|
317
|
+
tick("diff.done", { changeCount: changes.length });
|
|
318
|
+
return { agentId, from, to, changes };
|
|
319
|
+
}
|
|
320
|
+
catch (err) {
|
|
321
|
+
throw this.mapError(err, "failed to diff pack versions");
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/** Reconstruct pack without org-source hygiene (used to emit Magit deletes). */
|
|
325
|
+
async packAtCommitRaw(orgId, agentId, commitHash) {
|
|
326
|
+
this.ensureMongoEnv();
|
|
327
|
+
const repo = await magit.open({ repoRoot: this.repoRootFor(orgId) });
|
|
328
|
+
const seed = await this.resolveSeedCommit(repo);
|
|
329
|
+
const diff = await repo.diffBetweenCommits({
|
|
330
|
+
from: seed,
|
|
331
|
+
to: commitHash,
|
|
332
|
+
recordFull: true,
|
|
333
|
+
includeRemovedRecordBodies: false,
|
|
334
|
+
});
|
|
335
|
+
const col = diff.collections[MAGIT_AGENTS_COLLECTION];
|
|
336
|
+
const rows = (col?.records ?? [])
|
|
337
|
+
.filter((r) => r.after != null)
|
|
338
|
+
.map((r) => ({ id: r.id, after: r.after }));
|
|
339
|
+
const pack = recordsToPack(agentId, rows);
|
|
340
|
+
const checksum = checksumMetadataPack(pack);
|
|
341
|
+
return { pack, checksum, commit: commitHash };
|
|
342
|
+
}
|
|
343
|
+
async packAtCommit(orgId, agentId, commit) {
|
|
344
|
+
if (!agentId?.trim()) {
|
|
345
|
+
throw new ServiceError("VALIDATION", "agentId required", 400);
|
|
346
|
+
}
|
|
347
|
+
const commitHash = commit?.trim();
|
|
348
|
+
if (!commitHash) {
|
|
349
|
+
throw new ServiceError("VALIDATION", "commit required", 400);
|
|
350
|
+
}
|
|
351
|
+
await this.ensureRepo(orgId);
|
|
352
|
+
const { tick } = this.progress();
|
|
353
|
+
try {
|
|
354
|
+
tick("packAtCommit.start", { agentId, commit: commitHash });
|
|
355
|
+
const loaded = await this.packAtCommitRaw(orgId, agentId, commitHash);
|
|
356
|
+
const pack = stripOrgRuntimeSourcesFromPack(loaded.pack);
|
|
357
|
+
const checksum = checksumMetadataPack(pack);
|
|
358
|
+
return { pack, checksum, commit: commitHash };
|
|
359
|
+
}
|
|
360
|
+
catch (err) {
|
|
361
|
+
throw this.mapError(err, "failed to load pack at commit");
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Apply a Magit commit into live metadata Mongo (transform driver).
|
|
366
|
+
* Caller must rehydrate the Memorix metadata store afterward.
|
|
367
|
+
*/
|
|
368
|
+
async applyCommit(orgId, opts) {
|
|
369
|
+
const commitHash = opts.commit?.trim();
|
|
370
|
+
if (!commitHash) {
|
|
371
|
+
throw new ServiceError("VALIDATION", "commit required", 400);
|
|
372
|
+
}
|
|
373
|
+
await this.ensureRepo(orgId);
|
|
374
|
+
try {
|
|
375
|
+
this.ensureMongoEnv();
|
|
376
|
+
const repo = await magit.open({ repoRoot: this.repoRootFor(orgId) });
|
|
377
|
+
void opts.agentId;
|
|
378
|
+
const result = await repo.applyCommit(commitHash, {
|
|
379
|
+
mongoTarget: {
|
|
380
|
+
urlEnv: this.cfg.mongoUrlEnv,
|
|
381
|
+
db: this.cfg.metadataDb,
|
|
382
|
+
},
|
|
383
|
+
recordsMode: "exact",
|
|
384
|
+
});
|
|
385
|
+
return {
|
|
386
|
+
commit: result.commit,
|
|
387
|
+
summary: {
|
|
388
|
+
recordsUpserted: result.summary.recordsUpserted,
|
|
389
|
+
recordsDeleted: result.summary.recordsDeleted,
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
catch (err) {
|
|
394
|
+
throw this.mapError(err, "failed to apply pack commit");
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
async publishPlan(orgId, opts) {
|
|
398
|
+
const release = opts.release?.trim();
|
|
399
|
+
const commit = opts.commit?.trim();
|
|
400
|
+
const agentIds = opts.agentIds.map((a) => a.trim()).filter(Boolean);
|
|
401
|
+
if (!release)
|
|
402
|
+
throw new ServiceError("VALIDATION", "release name required", 400);
|
|
403
|
+
if (!commit)
|
|
404
|
+
throw new ServiceError("VALIDATION", "commit required", 400);
|
|
405
|
+
if (!agentIds.length) {
|
|
406
|
+
throw new ServiceError("VALIDATION", "agentIds required", 400);
|
|
407
|
+
}
|
|
408
|
+
await this.ensureRepo(orgId);
|
|
409
|
+
try {
|
|
410
|
+
this.ensureMongoEnv();
|
|
411
|
+
const repo = await magit.open({ repoRoot: this.repoRootFor(orgId) });
|
|
412
|
+
const plan = await repo.diffCommitToLive(commit, {
|
|
413
|
+
mongoTarget: {
|
|
414
|
+
urlEnv: this.cfg.mongoUrlEnv,
|
|
415
|
+
db: this.cfg.metadataDb,
|
|
416
|
+
},
|
|
417
|
+
recordFull: true,
|
|
418
|
+
only: agentIds.map((id) => `agentId=${id}`),
|
|
419
|
+
});
|
|
420
|
+
let toInsert = 0;
|
|
421
|
+
let toUpdate = 0;
|
|
422
|
+
let wouldDelete = 0;
|
|
423
|
+
const changes = [];
|
|
424
|
+
for (const agentId of agentIds) {
|
|
425
|
+
const col = plan.collections[MAGIT_AGENTS_COLLECTION];
|
|
426
|
+
if (!col)
|
|
427
|
+
continue;
|
|
428
|
+
toInsert += col.toInsert;
|
|
429
|
+
toUpdate += col.toUpdate;
|
|
430
|
+
wouldDelete += col.wouldDelete;
|
|
431
|
+
for (const row of col.records ?? []) {
|
|
432
|
+
const parsed = parseRecordId(row.id);
|
|
433
|
+
if (!parsed || parsed.agentId !== agentId)
|
|
434
|
+
continue;
|
|
435
|
+
let change = "changed";
|
|
436
|
+
if (row.before == null && row.after != null)
|
|
437
|
+
change = "added";
|
|
438
|
+
else if (row.before != null && row.after == null)
|
|
439
|
+
change = "removed";
|
|
440
|
+
const beforeBody = unwrapDefinitionBody(row.before);
|
|
441
|
+
const afterBody = unwrapDefinitionBody(row.after);
|
|
442
|
+
const fields = fieldDiffs(beforeBody, afterBody);
|
|
443
|
+
let labels;
|
|
444
|
+
try {
|
|
445
|
+
const labeled = labelDiff({
|
|
446
|
+
kind: parsed.kind,
|
|
447
|
+
id: parsed.id,
|
|
448
|
+
change,
|
|
449
|
+
before: beforeBody,
|
|
450
|
+
after: afterBody,
|
|
451
|
+
});
|
|
452
|
+
if (Array.isArray(labeled)) {
|
|
453
|
+
labels = labeled.map(String);
|
|
454
|
+
}
|
|
455
|
+
else if (labeled && typeof labeled === "object") {
|
|
456
|
+
const maybe = labeled.labels;
|
|
457
|
+
if (Array.isArray(maybe))
|
|
458
|
+
labels = maybe.map(String);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
462
|
+
/* optional enrichment */
|
|
463
|
+
}
|
|
464
|
+
changes.push({
|
|
465
|
+
kind: parsed.kind,
|
|
466
|
+
id: parsed.id,
|
|
467
|
+
change,
|
|
468
|
+
...(labels?.length ? { labels } : {}),
|
|
469
|
+
...(fields ? { fields } : {}),
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const planHash = createHash("sha256")
|
|
474
|
+
.update(JSON.stringify({ release, commit, agentIds, toInsert, toUpdate, wouldDelete }))
|
|
475
|
+
.digest("hex");
|
|
476
|
+
return {
|
|
477
|
+
release,
|
|
478
|
+
commit,
|
|
479
|
+
agentIds,
|
|
480
|
+
planHash,
|
|
481
|
+
summary: { toInsert, toUpdate, wouldDelete },
|
|
482
|
+
changes,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
catch (err) {
|
|
486
|
+
throw this.mapError(err, "failed to plan publish");
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
async recordRelease(orgId, release) {
|
|
490
|
+
await this.ensureRepo(orgId);
|
|
491
|
+
const path = this.releasesPath(orgId);
|
|
492
|
+
const all = this.readReleases(orgId);
|
|
493
|
+
const next = all.filter((r) => r.name !== release.name);
|
|
494
|
+
next.push(release);
|
|
495
|
+
next.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
496
|
+
writeFileSync(path, JSON.stringify({ releases: next }, null, 2), "utf8");
|
|
497
|
+
return release;
|
|
498
|
+
}
|
|
499
|
+
listReleases(orgId) {
|
|
500
|
+
return this.readReleases(orgId);
|
|
501
|
+
}
|
|
502
|
+
releasesPath(orgId) {
|
|
503
|
+
return join(this.repoRootFor(orgId), ".magit", RELEASES_FILE);
|
|
504
|
+
}
|
|
505
|
+
readReleases(orgId) {
|
|
506
|
+
const path = this.releasesPath(orgId);
|
|
507
|
+
if (!existsSync(path))
|
|
508
|
+
return [];
|
|
509
|
+
try {
|
|
510
|
+
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
511
|
+
return Array.isArray(raw.releases) ? raw.releases : [];
|
|
512
|
+
}
|
|
513
|
+
catch {
|
|
514
|
+
return [];
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
async resolveSeedCommit(repo) {
|
|
518
|
+
const commits = await repo.listCommitsFromHead({ maxCount: 200 });
|
|
519
|
+
const seed = [...commits]
|
|
520
|
+
.reverse()
|
|
521
|
+
.find((c) => (c.message ?? "").includes("seed metadata"));
|
|
522
|
+
if (seed?.hash)
|
|
523
|
+
return seed.hash;
|
|
524
|
+
const oldest = commits[commits.length - 1];
|
|
525
|
+
if (!oldest?.hash) {
|
|
526
|
+
throw new ServiceError("MAGIT_UNAVAILABLE", "Magit repo has no commits", 503);
|
|
527
|
+
}
|
|
528
|
+
return oldest.hash;
|
|
529
|
+
}
|
|
530
|
+
async resolveCommitRef(repo, ref) {
|
|
531
|
+
if (isHeadRef(ref)) {
|
|
532
|
+
const head = await repo.listCommitsFromHead({ maxCount: 1 });
|
|
533
|
+
const hash = head[0]?.hash;
|
|
534
|
+
if (!hash) {
|
|
535
|
+
throw new ServiceError("VALIDATION", "HEAD has no commits", 400);
|
|
536
|
+
}
|
|
537
|
+
return hash;
|
|
538
|
+
}
|
|
539
|
+
return ref;
|
|
540
|
+
}
|
|
541
|
+
async collectDiffRows(repo, agentId, from, to) {
|
|
542
|
+
const rows = [];
|
|
543
|
+
const pushRows = (records, infer) => {
|
|
544
|
+
for (const r of records ?? []) {
|
|
545
|
+
const parsed = parseRecordId(r.id);
|
|
546
|
+
if (!parsed || parsed.agentId !== agentId)
|
|
547
|
+
continue;
|
|
548
|
+
if (parsed.agentId === MAGIT_SEED_AGENT_ID)
|
|
549
|
+
continue;
|
|
550
|
+
const change = infer(r);
|
|
551
|
+
if (!change)
|
|
552
|
+
continue;
|
|
553
|
+
const beforeBody = unwrapDefinitionBody(r.before);
|
|
554
|
+
const afterBody = unwrapDefinitionBody(r.after);
|
|
555
|
+
const fields = fieldDiffs(beforeBody, afterBody);
|
|
556
|
+
let labels;
|
|
557
|
+
try {
|
|
558
|
+
const labeled = labelDiff({
|
|
559
|
+
kind: parsed.kind,
|
|
560
|
+
id: parsed.id,
|
|
561
|
+
change,
|
|
562
|
+
before: beforeBody,
|
|
563
|
+
after: afterBody,
|
|
564
|
+
});
|
|
565
|
+
if (Array.isArray(labeled))
|
|
566
|
+
labels = labeled.map(String);
|
|
567
|
+
else if (labeled && typeof labeled === "object") {
|
|
568
|
+
const maybe = labeled.labels;
|
|
569
|
+
if (Array.isArray(maybe))
|
|
570
|
+
labels = maybe.map(String);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
catch {
|
|
574
|
+
/* optional */
|
|
575
|
+
}
|
|
576
|
+
rows.push({
|
|
577
|
+
kind: parsed.kind,
|
|
578
|
+
id: parsed.id,
|
|
579
|
+
change,
|
|
580
|
+
...(labels?.length ? { labels } : {}),
|
|
581
|
+
...(fields ? { fields } : {}),
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
if (isLiveRef(from) && isLiveRef(to)) {
|
|
586
|
+
return [];
|
|
587
|
+
}
|
|
588
|
+
if (isLiveRef(to) || isLiveRef(from)) {
|
|
589
|
+
const commitSide = isLiveRef(to) ? from : to;
|
|
590
|
+
const commitHash = await this.resolveCommitRef(repo, commitSide);
|
|
591
|
+
const plan = await repo.diffCommitToLive(commitHash, {
|
|
592
|
+
mongoTarget: {
|
|
593
|
+
urlEnv: this.cfg.mongoUrlEnv,
|
|
594
|
+
db: this.cfg.metadataDb,
|
|
595
|
+
},
|
|
596
|
+
recordFull: true,
|
|
597
|
+
only: [`agentId=${agentId}`],
|
|
598
|
+
});
|
|
599
|
+
const col = plan.collections[MAGIT_AGENTS_COLLECTION];
|
|
600
|
+
const invert = isLiveRef(from);
|
|
601
|
+
pushRows(col?.records, (r) => {
|
|
602
|
+
const hasBefore = r.before != null || Boolean(r.beforeHash);
|
|
603
|
+
const hasAfter = r.after != null || Boolean(r.afterHash);
|
|
604
|
+
let change = null;
|
|
605
|
+
if (!hasBefore && hasAfter)
|
|
606
|
+
change = "added";
|
|
607
|
+
else if (hasBefore && !hasAfter)
|
|
608
|
+
change = "removed";
|
|
609
|
+
else if (hasBefore && hasAfter)
|
|
610
|
+
change = "changed";
|
|
611
|
+
if (!change)
|
|
612
|
+
return null;
|
|
613
|
+
if (!invert)
|
|
614
|
+
return change;
|
|
615
|
+
if (change === "added")
|
|
616
|
+
return "removed";
|
|
617
|
+
if (change === "removed")
|
|
618
|
+
return "added";
|
|
619
|
+
return "changed";
|
|
620
|
+
});
|
|
621
|
+
return rows;
|
|
622
|
+
}
|
|
623
|
+
const fromHash = await this.resolveCommitRef(repo, from);
|
|
624
|
+
const toHash = await this.resolveCommitRef(repo, to);
|
|
625
|
+
const diff = await repo.diffBetweenCommits({
|
|
626
|
+
from: fromHash,
|
|
627
|
+
to: toHash,
|
|
628
|
+
recordFull: true,
|
|
629
|
+
includeRemovedRecordBodies: true,
|
|
630
|
+
});
|
|
631
|
+
const col = diff.collections[MAGIT_AGENTS_COLLECTION];
|
|
632
|
+
pushRows(col?.records, (r) => {
|
|
633
|
+
const hasBefore = r.before != null || Boolean(r.beforeHash);
|
|
634
|
+
const hasAfter = r.after != null || Boolean(r.afterHash);
|
|
635
|
+
if (!hasBefore && hasAfter)
|
|
636
|
+
return "added";
|
|
637
|
+
if (hasBefore && !hasAfter)
|
|
638
|
+
return "removed";
|
|
639
|
+
if (hasBefore && hasAfter)
|
|
640
|
+
return "changed";
|
|
641
|
+
return null;
|
|
642
|
+
});
|
|
643
|
+
return rows;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Snapshot a temporary seed agent so HEAD carries `memorix_metadata_agents`,
|
|
647
|
+
* then remove the seed from live Mongo (saveVersion must stay non-live).
|
|
648
|
+
*/
|
|
649
|
+
async seedAgentsCollectionCommit(repo) {
|
|
650
|
+
this.ensureMongoEnv();
|
|
651
|
+
const client = new MongoClient(this.cfg.mongoUri);
|
|
652
|
+
await client.connect();
|
|
653
|
+
try {
|
|
654
|
+
const col = client
|
|
655
|
+
.db(this.cfg.metadataDb)
|
|
656
|
+
.collection(MAGIT_AGENTS_COLLECTION);
|
|
657
|
+
await col.updateOne({ _id: MAGIT_SEED_AGENT_ID }, {
|
|
658
|
+
$set: {
|
|
659
|
+
agentId: MAGIT_SEED_AGENT_ID,
|
|
660
|
+
checksum: "magit-seed",
|
|
661
|
+
definitions: [
|
|
662
|
+
{
|
|
663
|
+
kind: "agents",
|
|
664
|
+
id: MAGIT_SEED_AGENT_ID,
|
|
665
|
+
definition: {
|
|
666
|
+
id: MAGIT_SEED_AGENT_ID,
|
|
667
|
+
name: "magit-seed",
|
|
668
|
+
parentId: null,
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
],
|
|
672
|
+
},
|
|
673
|
+
}, { upsert: true });
|
|
674
|
+
await repo.commit({ message: "memorix: seed metadata collections" });
|
|
675
|
+
await col.deleteOne({ _id: MAGIT_SEED_AGENT_ID });
|
|
676
|
+
}
|
|
677
|
+
finally {
|
|
678
|
+
await client.close();
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
ensureMongoEnv() {
|
|
682
|
+
if (!process.env[this.cfg.mongoUrlEnv]?.trim()) {
|
|
683
|
+
process.env[this.cfg.mongoUrlEnv] = this.cfg.mongoUri;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
mapError(err, fallback) {
|
|
687
|
+
if (err instanceof ServiceError)
|
|
688
|
+
return err;
|
|
689
|
+
if (isPatchsetError(err)) {
|
|
690
|
+
return new ServiceError("VALIDATION", `${fallback}: ${err.message}`, 400);
|
|
691
|
+
}
|
|
692
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
693
|
+
return new ServiceError("MAGIT_UNAVAILABLE", `${fallback}: ${message}`, 503);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
export function createMagitPackService(cfg) {
|
|
697
|
+
return new MagitPackService(cfg);
|
|
698
|
+
}
|
|
699
|
+
//# sourceMappingURL=magit-pack-service.js.map
|