@x12i/memorix-service 3.12.3 → 3.13.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.
Files changed (211) hide show
  1. package/README.md +2 -2
  2. package/dist/activity-ui.js +1 -1
  3. package/dist/agent-catalog.d.ts +32 -0
  4. package/dist/agent-catalog.d.ts.map +1 -0
  5. package/dist/agent-catalog.js +80 -0
  6. package/dist/agent-catalog.js.map +1 -0
  7. package/dist/analytics/engine.d.ts.map +1 -1
  8. package/dist/analytics/engine.js +2 -1
  9. package/dist/analytics/engine.js.map +1 -1
  10. package/dist/analytics/state.d.ts +1 -1
  11. package/dist/analytics/tracker.d.ts +2 -0
  12. package/dist/analytics/tracker.d.ts.map +1 -1
  13. package/dist/analytics/tracker.js +15 -8
  14. package/dist/analytics/tracker.js.map +1 -1
  15. package/dist/anomaly/catalog.d.ts +4 -0
  16. package/dist/anomaly/catalog.d.ts.map +1 -0
  17. package/dist/anomaly/catalog.js +315 -0
  18. package/dist/anomaly/catalog.js.map +1 -0
  19. package/dist/anomaly/descriptors.d.ts +11 -0
  20. package/dist/anomaly/descriptors.d.ts.map +1 -0
  21. package/dist/anomaly/descriptors.js +113 -0
  22. package/dist/anomaly/descriptors.js.map +1 -0
  23. package/dist/anomaly/index.d.ts +6 -0
  24. package/dist/anomaly/index.d.ts.map +1 -0
  25. package/dist/anomaly/index.js +5 -0
  26. package/dist/anomaly/index.js.map +1 -0
  27. package/dist/anomaly/records.d.ts +28 -0
  28. package/dist/anomaly/records.d.ts.map +1 -0
  29. package/dist/anomaly/records.js +384 -0
  30. package/dist/anomaly/records.js.map +1 -0
  31. package/dist/anomaly/types.d.ts +116 -0
  32. package/dist/anomaly/types.d.ts.map +1 -0
  33. package/dist/anomaly/types.js +24 -0
  34. package/dist/anomaly/types.js.map +1 -0
  35. package/dist/anomaly/values.d.ts +31 -0
  36. package/dist/anomaly/values.d.ts.map +1 -0
  37. package/dist/anomaly/values.js +171 -0
  38. package/dist/anomaly/values.js.map +1 -0
  39. package/dist/app.d.ts.map +1 -1
  40. package/dist/app.js +34 -1
  41. package/dist/app.js.map +1 -1
  42. package/dist/arrivals-backfill.d.ts +23 -0
  43. package/dist/arrivals-backfill.d.ts.map +1 -0
  44. package/dist/arrivals-backfill.js +132 -0
  45. package/dist/arrivals-backfill.js.map +1 -0
  46. package/dist/arrivals-routes.d.ts +8 -0
  47. package/dist/arrivals-routes.d.ts.map +1 -0
  48. package/dist/arrivals-routes.js +17 -0
  49. package/dist/arrivals-routes.js.map +1 -0
  50. package/dist/arrivals-ui.d.ts +3 -0
  51. package/dist/arrivals-ui.d.ts.map +1 -0
  52. package/dist/arrivals-ui.js +277 -0
  53. package/dist/arrivals-ui.js.map +1 -0
  54. package/dist/catalog-bridge.d.ts +18 -0
  55. package/dist/catalog-bridge.d.ts.map +1 -1
  56. package/dist/catalog-bridge.js +115 -7
  57. package/dist/catalog-bridge.js.map +1 -1
  58. package/dist/catalog-live-ui.js +1 -1
  59. package/dist/content-arrivals-countex.d.ts +90 -0
  60. package/dist/content-arrivals-countex.d.ts.map +1 -0
  61. package/dist/content-arrivals-countex.js +252 -0
  62. package/dist/content-arrivals-countex.js.map +1 -0
  63. package/dist/data-changes-ui.js +1 -1
  64. package/dist/data-changes.d.ts +3 -0
  65. package/dist/data-changes.d.ts.map +1 -1
  66. package/dist/data-changes.js +22 -21
  67. package/dist/data-changes.js.map +1 -1
  68. package/dist/errors.d.ts +6 -1
  69. package/dist/errors.d.ts.map +1 -1
  70. package/dist/errors.js +22 -2
  71. package/dist/errors.js.map +1 -1
  72. package/dist/hooks-live-ui.js +1 -1
  73. package/dist/knowledge-upstream.d.ts +32 -0
  74. package/dist/knowledge-upstream.d.ts.map +1 -1
  75. package/dist/knowledge-upstream.js +86 -2
  76. package/dist/knowledge-upstream.js.map +1 -1
  77. package/dist/land-v2-guards.d.ts +5 -12
  78. package/dist/land-v2-guards.d.ts.map +1 -1
  79. package/dist/land-v2-guards.js +10 -33
  80. package/dist/land-v2-guards.js.map +1 -1
  81. package/dist/live-ops-nav.d.ts.map +1 -1
  82. package/dist/live-ops-nav.js +4 -0
  83. package/dist/live-ops-nav.js.map +1 -1
  84. package/dist/mcp/nexum-tools.d.ts +1 -1
  85. package/dist/mcp/nexum-tools.d.ts.map +1 -1
  86. package/dist/mcp/nexum-tools.js +39 -22
  87. package/dist/mcp/nexum-tools.js.map +1 -1
  88. package/dist/nexum-analysis/enqueue.d.ts.map +1 -1
  89. package/dist/nexum-analysis/enqueue.js +2 -1
  90. package/dist/nexum-analysis/enqueue.js.map +1 -1
  91. package/dist/nexum-analysis/knowledge-read.d.ts +45 -0
  92. package/dist/nexum-analysis/knowledge-read.d.ts.map +1 -0
  93. package/dist/nexum-analysis/knowledge-read.js +26 -0
  94. package/dist/nexum-analysis/knowledge-read.js.map +1 -0
  95. package/dist/nexum-analysis/outbox.d.ts +1 -1
  96. package/dist/nexum-analysis/reconcile.d.ts.map +1 -1
  97. package/dist/nexum-analysis/reconcile.js +3 -2
  98. package/dist/nexum-analysis/reconcile.js.map +1 -1
  99. package/dist/openapi/openapi.json +18354 -17673
  100. package/dist/operational-db.d.ts +2 -2
  101. package/dist/operational-db.d.ts.map +1 -1
  102. package/dist/operational-db.js +8 -11
  103. package/dist/operational-db.js.map +1 -1
  104. package/dist/patches/access.d.ts +15 -0
  105. package/dist/patches/access.d.ts.map +1 -0
  106. package/dist/patches/access.js +27 -0
  107. package/dist/patches/access.js.map +1 -0
  108. package/dist/patches/engine.d.ts +67 -0
  109. package/dist/patches/engine.d.ts.map +1 -0
  110. package/dist/patches/engine.js +966 -0
  111. package/dist/patches/engine.js.map +1 -0
  112. package/dist/patches/guards.d.ts +23 -0
  113. package/dist/patches/guards.d.ts.map +1 -0
  114. package/dist/patches/guards.js +77 -0
  115. package/dist/patches/guards.js.map +1 -0
  116. package/dist/patches/legacy-rows.d.ts +56 -0
  117. package/dist/patches/legacy-rows.d.ts.map +1 -0
  118. package/dist/patches/legacy-rows.js +162 -0
  119. package/dist/patches/legacy-rows.js.map +1 -0
  120. package/dist/patches/ops.d.ts +24 -0
  121. package/dist/patches/ops.d.ts.map +1 -0
  122. package/dist/patches/ops.js +204 -0
  123. package/dist/patches/ops.js.map +1 -0
  124. package/dist/patches/parse.d.ts +5 -0
  125. package/dist/patches/parse.d.ts.map +1 -0
  126. package/dist/patches/parse.js +323 -0
  127. package/dist/patches/parse.js.map +1 -0
  128. package/dist/patches/paths.d.ts +38 -0
  129. package/dist/patches/paths.d.ts.map +1 -0
  130. package/dist/patches/paths.js +204 -0
  131. package/dist/patches/paths.js.map +1 -0
  132. package/dist/patches/service.d.ts +16 -0
  133. package/dist/patches/service.d.ts.map +1 -0
  134. package/dist/patches/service.js +128 -0
  135. package/dist/patches/service.js.map +1 -0
  136. package/dist/patches/store.d.ts +87 -0
  137. package/dist/patches/store.d.ts.map +1 -0
  138. package/dist/patches/store.js +27 -0
  139. package/dist/patches/store.js.map +1 -0
  140. package/dist/patches/types.d.ts +112 -0
  141. package/dist/patches/types.d.ts.map +1 -0
  142. package/dist/patches/types.js +5 -0
  143. package/dist/patches/types.js.map +1 -0
  144. package/dist/platform.d.ts +17 -3
  145. package/dist/platform.d.ts.map +1 -1
  146. package/dist/platform.js +114 -11
  147. package/dist/platform.js.map +1 -1
  148. package/dist/routes/anomaly.d.ts +5 -0
  149. package/dist/routes/anomaly.d.ts.map +1 -0
  150. package/dist/routes/anomaly.js +308 -0
  151. package/dist/routes/anomaly.js.map +1 -0
  152. package/dist/routes/data.d.ts.map +1 -1
  153. package/dist/routes/data.js +252 -210
  154. package/dist/routes/data.js.map +1 -1
  155. package/dist/routes/knowledge.d.ts.map +1 -1
  156. package/dist/routes/knowledge.js +6 -22
  157. package/dist/routes/knowledge.js.map +1 -1
  158. package/dist/routes/memory.d.ts.map +1 -1
  159. package/dist/routes/memory.js +2 -4
  160. package/dist/routes/memory.js.map +1 -1
  161. package/dist/routes/metadata.d.ts.map +1 -1
  162. package/dist/routes/metadata.js +40 -1
  163. package/dist/routes/metadata.js.map +1 -1
  164. package/dist/routes/mgc-landing.d.ts.map +1 -1
  165. package/dist/routes/mgc-landing.js +64 -94
  166. package/dist/routes/mgc-landing.js.map +1 -1
  167. package/dist/routes/patches.d.ts +7 -0
  168. package/dist/routes/patches.d.ts.map +1 -0
  169. package/dist/routes/patches.js +111 -0
  170. package/dist/routes/patches.js.map +1 -0
  171. package/dist/routes/subject-roots-backfill.d.ts +4 -0
  172. package/dist/routes/subject-roots-backfill.d.ts.map +1 -0
  173. package/dist/routes/subject-roots-backfill.js +15 -0
  174. package/dist/routes/subject-roots-backfill.js.map +1 -0
  175. package/dist/routes/workflow.d.ts.map +1 -1
  176. package/dist/routes/workflow.js +4 -10
  177. package/dist/routes/workflow.js.map +1 -1
  178. package/dist/single-read-discriminator.d.ts +17 -0
  179. package/dist/single-read-discriminator.d.ts.map +1 -0
  180. package/dist/single-read-discriminator.js +67 -0
  181. package/dist/single-read-discriminator.js.map +1 -0
  182. package/dist/subject-roots-backfill.d.ts +24 -0
  183. package/dist/subject-roots-backfill.d.ts.map +1 -0
  184. package/dist/subject-roots-backfill.js +133 -0
  185. package/dist/subject-roots-backfill.js.map +1 -0
  186. package/dist/write-precondition.d.ts +5 -1
  187. package/dist/write-precondition.d.ts.map +1 -1
  188. package/dist/write-precondition.js +11 -3
  189. package/dist/write-precondition.js.map +1 -1
  190. package/dist/writes/content-write.d.ts +170 -0
  191. package/dist/writes/content-write.d.ts.map +1 -0
  192. package/dist/writes/content-write.js +569 -0
  193. package/dist/writes/content-write.js.map +1 -0
  194. package/dist/writes/effects.d.ts +13 -0
  195. package/dist/writes/effects.d.ts.map +1 -0
  196. package/dist/writes/effects.js +28 -0
  197. package/dist/writes/effects.js.map +1 -0
  198. package/dist/writes/error-shape.d.ts +11 -0
  199. package/dist/writes/error-shape.d.ts.map +1 -0
  200. package/dist/writes/error-shape.js +100 -0
  201. package/dist/writes/error-shape.js.map +1 -0
  202. package/dist/writes/receipt-store.d.ts +81 -0
  203. package/dist/writes/receipt-store.d.ts.map +1 -0
  204. package/dist/writes/receipt-store.js +171 -0
  205. package/dist/writes/receipt-store.js.map +1 -0
  206. package/dist/writes/worker.d.ts +9 -0
  207. package/dist/writes/worker.d.ts.map +1 -0
  208. package/dist/writes/worker.js +20 -0
  209. package/dist/writes/worker.js.map +1 -0
  210. package/openapi/openapi.json +18354 -17673
  211. package/package.json +4 -4
@@ -2,13 +2,31 @@ import { parseLiveDeclaration } from "@x12i/memorix-data";
2
2
  import { resolveScope } from "../request-scope.js";
3
3
  import { ServiceError } from "../errors.js";
4
4
  import { requireCallbackAccess, requireDeploymentAccess } from "../access-gate.js";
5
- import { normalizeWritePrecondition } from "../write-precondition.js";
6
- import { assertAddressMatchesEnvelope, resolveRecordIdAlias, } from "../identity-alias.js";
5
+ import { runContentBatch, runContentWrite } from "../writes/content-write.js";
6
+ import { commitWriteEffects } from "../writes/effects.js";
7
+ import { patchGuardCheck } from "../patches/service.js";
8
+ import { resolveRecordIdAlias } from "../identity-alias.js";
7
9
  import { maybeEnqueueNexumAnalysis } from "../nexum-analysis/enqueue.js";
10
+ import { listKnowledgeGraphsForSource } from "../nexum-analysis/knowledge-read.js";
8
11
  import { memorixActivityRing, recordLooksLikeKnowledgeGraph, } from "../activity-ring.js";
9
12
  import { getRequestCaller, requireCaller } from "../caller-identity.js";
10
- import { runHookTrigger } from "../hooks/request-trigger.js";
11
13
  import { parseSafeFilters, parseSafeFiltersValue, parseSafeSort, parseSafeSortValue } from "../safe-query.js";
14
+ import { declaredDiscriminatorValues } from "../catalog-bridge.js";
15
+ import { singleReadDiscriminatorValue, withDiscriminatorHelp } from "../single-read-discriminator.js";
16
+ function parseIncludeObject(raw) {
17
+ if (raw === undefined)
18
+ return undefined;
19
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
20
+ throw new ServiceError("VALIDATION", "include must be an object", 400);
21
+ }
22
+ const markdown = raw.markdown;
23
+ if (markdown === undefined)
24
+ return {};
25
+ if (typeof markdown !== "boolean") {
26
+ throw new ServiceError("VALIDATION", "include.markdown must be a boolean", 400);
27
+ }
28
+ return { markdown };
29
+ }
12
30
  function parseContentInclude(raw) {
13
31
  if (raw === undefined || raw === "")
14
32
  return undefined;
@@ -19,24 +37,39 @@ function parseContentInclude(raw) {
19
37
  catch {
20
38
  throw new ServiceError("VALIDATION", "include must be JSON", 400);
21
39
  }
22
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
23
- throw new ServiceError("VALIDATION", "include must be an object", 400);
24
- }
25
- const markdown = parsed.markdown;
26
- if (markdown === undefined)
27
- return {};
28
- if (typeof markdown !== "boolean") {
29
- throw new ServiceError("VALIDATION", "include.markdown must be a boolean", 400);
30
- }
31
- return { markdown };
40
+ return parseIncludeObject(parsed);
32
41
  }
33
- function readSuppressHooks(value) {
34
- if (value === undefined)
35
- return false;
36
- if (typeof value !== "boolean") {
37
- throw new ServiceError("VALIDATION", "suppressHooks must be a boolean", 400);
42
+ /** Wildcard path so record ids that contain `/` stay intact. */
43
+ function subjectRecordIdFromRequest(req) {
44
+ const params = (req.params ?? {});
45
+ const raw = params["*"] ?? params.path ?? "";
46
+ const recordId = raw.trim();
47
+ if (!recordId) {
48
+ throw new ServiceError("VALIDATION", "recordId required", 400);
38
49
  }
39
- return value;
50
+ return recordId;
51
+ }
52
+ function publicWriteReceipt(row) {
53
+ return {
54
+ receiptId: row.receiptId,
55
+ status: row.status,
56
+ ...(row.writeId ? { writeId: row.writeId } : {}),
57
+ createdAt: row.createdAt,
58
+ updatedAt: row.updatedAt,
59
+ ...(row.receipt ? { receipt: row.receipt } : {}),
60
+ ...(row.error
61
+ ? {
62
+ error: {
63
+ code: row.error.code,
64
+ field: row.error.field,
65
+ message: row.error.message,
66
+ retryable: row.error.retryable,
67
+ ...(row.error.retryAfterMs !== undefined ? { retryAfterMs: row.error.retryAfterMs } : {}),
68
+ },
69
+ }
70
+ : {}),
71
+ ...(row.status === "pending" ? { retryAfterMs: row.error?.retryAfterMs ?? 1_000 } : {}),
72
+ };
40
73
  }
41
74
  function contentDatasource(req, body) {
42
75
  const service = requireCaller(req);
@@ -60,7 +93,79 @@ function draftForDatasourceStamp(record) {
60
93
  ...(record.insights ? { insights: record.insights } : {}),
61
94
  };
62
95
  }
96
+ /** One content instance by recordId; `filter` may only select the discriminator. */
97
+ async function getSingleContent(platform, scope, q, recordId, include) {
98
+ const ports = platform.portsFor(scope);
99
+ const discriminatorPath = ports.catalog.getContentProfile?.(q.contentType)?.discriminator?.path;
100
+ const discriminatorValue = singleReadDiscriminatorValue({
101
+ discriminatorValue: q.discriminatorValue ?? q.discriminator,
102
+ filters: parseSafeFilters(q.filter),
103
+ discriminatorPath,
104
+ });
105
+ try {
106
+ const record = (await ports.reader.getContent(scope, {
107
+ objectType: q.objectType,
108
+ contentType: q.contentType,
109
+ recordId,
110
+ ...(discriminatorValue ? { discriminatorValue } : {}),
111
+ }, include ? { include } : undefined));
112
+ return { record, ...(discriminatorValue ? { discriminatorValue } : {}) };
113
+ }
114
+ catch (err) {
115
+ throw withDiscriminatorHelp(err, () => discriminatorPath
116
+ ? declaredDiscriminatorValues(platform.metadata, scope.agentIds, q.objectType, q.contentType, discriminatorPath)
117
+ : undefined);
118
+ }
119
+ }
63
120
  export async function registerDataRoutes(app, platform) {
121
+ app.get("/api/data/knowledge-graphs", async (req) => {
122
+ requireCallbackAccess(req, "explore");
123
+ const scope = resolveScope(req);
124
+ const q = req.query;
125
+ const sourceRecordId = q.sourceRecordId?.trim() ?? "";
126
+ if (!sourceRecordId) {
127
+ throw new ServiceError("VALIDATION", "sourceRecordId required", 400);
128
+ }
129
+ const caller = getRequestCaller(req);
130
+ const ports = platform.portsFor(scope);
131
+ const listed = await listKnowledgeGraphsForSource({
132
+ reader: ports.reader,
133
+ scope,
134
+ sourceRecordId,
135
+ limit: Number(q.limit ?? 50) || 50,
136
+ cursor: q.cursor,
137
+ });
138
+ memorixActivityRing.recordFetch({
139
+ orgId: scope.orgId,
140
+ caller,
141
+ mode: "list",
142
+ objectType: listed.addressing.objectType,
143
+ contentType: listed.addressing.contentType,
144
+ recordId: sourceRecordId,
145
+ hit: listed.records.length > 0,
146
+ count: listed.records.length,
147
+ knowledgeGraph: listed.records.length > 0,
148
+ });
149
+ return { scope, ...listed };
150
+ });
151
+ app.get("/api/data/content/context", async (req) => {
152
+ requireCallbackAccess(req, "explore");
153
+ const scope = resolveScope(req);
154
+ const q = req.query;
155
+ if (!q.objectType || !q.contentType) {
156
+ throw new ServiceError("VALIDATION", "objectType and contentType required", 400);
157
+ }
158
+ const recordId = resolveRecordIdAlias(q.id, q.recordId);
159
+ if (!recordId) {
160
+ throw new ServiceError("VALIDATION", "recordId required", 400);
161
+ }
162
+ const { record } = await getSingleContent(platform, scope, { ...q, objectType: q.objectType, contentType: q.contentType }, recordId);
163
+ const context = record.context;
164
+ if (!context || typeof context !== "object" || Array.isArray(context) || Object.keys(context).length === 0) {
165
+ throw new ServiceError("NOT_FOUND", "context is not stored on this record", 404);
166
+ }
167
+ return context;
168
+ });
64
169
  app.get("/api/data/content", async (req) => {
65
170
  requireCallbackAccess(req, "explore");
66
171
  const scope = resolveScope(req);
@@ -75,12 +180,9 @@ export async function registerDataRoutes(app, platform) {
75
180
  const recordId = resolveRecordIdAlias(q.id, q.recordId);
76
181
  if (recordId) {
77
182
  let record;
183
+ let discriminatorValue;
78
184
  try {
79
- record = (await ports.reader.getContent(scope, {
80
- objectType: q.objectType,
81
- contentType: q.contentType,
82
- recordId,
83
- }, { include }));
185
+ ({ record, discriminatorValue } = await getSingleContent(platform, scope, { ...q, objectType: q.objectType, contentType: q.contentType }, recordId, include));
84
186
  }
85
187
  catch (err) {
86
188
  const msg = err instanceof Error ? err.message : String(err);
@@ -126,6 +228,7 @@ export async function registerDataRoutes(app, platform) {
126
228
  contentType: q.contentType,
127
229
  id: recordId,
128
230
  recordId,
231
+ ...(discriminatorValue ? { discriminatorValue } : {}),
129
232
  },
130
233
  record,
131
234
  currentRevision: record.revision,
@@ -135,25 +238,32 @@ export async function registerDataRoutes(app, platform) {
135
238
  const limit = Math.min(Math.max(Number(q.limit ?? 50) || 50, 1), 200);
136
239
  const filters = parseSafeFilters(q.filter);
137
240
  const sort = parseSafeSort(q.sort);
138
- if (q.count === "true" && platform.analytics) {
139
- const count = await platform.analytics.countIndexed(scope, {
140
- objectType: q.objectType,
141
- contentType: q.contentType,
142
- filters,
143
- });
144
- if (count !== null) {
145
- return {
146
- scope,
147
- count,
148
- addressing: {
149
- orgId: scope.orgId,
150
- objectType: q.objectType,
151
- contentType: q.contentType,
152
- id: null,
153
- recordId: null,
154
- },
155
- };
156
- }
241
+ if (q.count === "true") {
242
+ const indexed = platform.analytics
243
+ ? await platform.analytics.countIndexed(scope, {
244
+ objectType: q.objectType,
245
+ contentType: q.contentType,
246
+ filters,
247
+ })
248
+ : null;
249
+ const count = indexed !== null
250
+ ? indexed
251
+ : await ports.reader.countContent(scope, {
252
+ objectType: q.objectType,
253
+ contentType: q.contentType,
254
+ filters,
255
+ });
256
+ return {
257
+ scope,
258
+ count,
259
+ addressing: {
260
+ orgId: scope.orgId,
261
+ objectType: q.objectType,
262
+ contentType: q.contentType,
263
+ id: null,
264
+ recordId: null,
265
+ },
266
+ };
157
267
  }
158
268
  const listed = await ports.reader.listContent(scope, {
159
269
  objectType: q.objectType,
@@ -222,174 +332,71 @@ export async function registerDataRoutes(app, platform) {
222
332
  pageInfo: { nextCursor: listed.nextCursor ?? null, limit },
223
333
  };
224
334
  });
225
- app.post("/api/data/content", async (req) => {
335
+ app.post("/api/data/content/batch", async (req) => {
226
336
  requireDeploymentAccess(req, "write");
227
337
  const scope = resolveScope(req);
228
- const body = req.body;
229
- if (!body.objectType || !body.contentType) {
230
- throw new ServiceError("VALIDATION", "objectType, contentType required", 400);
231
- }
232
338
  await platform.analytics?.prepare(scope.orgId);
233
- assertAddressMatchesEnvelope({
234
- objectType: body.objectType,
235
- contentType: body.contentType,
236
- record: body.record,
339
+ const caller = requireCaller(req);
340
+ return runContentBatch({
341
+ scope,
342
+ caller,
343
+ datasource: contentDatasource(req, {}),
344
+ ports: platform.portsFor(scope),
345
+ receipts: platform.writeReceiptsFor(scope.orgId),
346
+ onCommitted: (event) => commitWriteEffects({ platform, scope, caller, event }),
347
+ checkWrite: patchGuardCheck(platform, scope),
348
+ }, req.body, (item) => contentDatasource(req, item));
349
+ });
350
+ app.get("/api/data/receipts", async (req) => {
351
+ requireCallbackAccess(req, "explore");
352
+ const scope = resolveScope(req);
353
+ const caller = requireCaller(req);
354
+ const q = req.query;
355
+ const status = q.status === "pending" || q.status === "stored" || q.status === "rejected" || q.status === "all"
356
+ ? q.status
357
+ : "rejected";
358
+ const limit = Math.min(Math.max(Number(q.limit ?? 50) || 50, 1), 200);
359
+ const rows = await platform.writeReceiptsFor(scope.orgId).list({
360
+ orgId: scope.orgId,
361
+ caller,
362
+ ...(status === "all" ? {} : { status }),
363
+ limit,
237
364
  });
238
- const precondition = normalizeWritePrecondition(body.precondition, body.expectedRevision);
239
- const datasource = contentDatasource(req, body);
240
- const suppressHooks = readSuppressHooks(body.suppressHooks);
241
- const ports = platform.portsFor(scope);
242
- const op = body.op ?? (precondition.form === "absent" ? "add" : "upsert");
243
- const expectedRevision = precondition.form === "exact-revision" ? precondition.expectedRevision : undefined;
244
- try {
245
- if (op === "remove") {
246
- if (!body.record?.recordId || expectedRevision === undefined) {
247
- throw new ServiceError("VALIDATION", "record.recordId and expectedRevision required for remove", 400);
248
- }
249
- const pinned = await ports.publications.list();
250
- const blocking = pinned.filter((p) => (p.status === "committed" || p.status === "draft") &&
251
- p.members.some((m) => m.recordId === body.record.recordId &&
252
- m.objectType === body.objectType &&
253
- m.contentType === body.contentType));
254
- if (blocking.length) {
255
- throw new ServiceError("PUBLICATION_PINNED", "cannot remove a content instance referenced by a draft or committed publication", 409, { publicationIds: blocking.map((p) => p.publicationId) });
256
- }
257
- const objectType = body.objectType;
258
- const contentType = body.contentType;
259
- const recordId = body.record.recordId;
260
- const noted = await runHookTrigger(() => ports.writer.remove({
261
- scope,
262
- objectType,
263
- contentType,
264
- recordId,
265
- precondition: { kind: "revision", revision: expectedRevision },
266
- datasource,
267
- suppressHooks,
268
- }));
269
- const result = noted.value;
270
- memorixActivityRing.recordLand({
271
- orgId: scope.orgId,
272
- caller: getRequestCaller(req),
273
- objectType: body.objectType,
274
- contentType: body.contentType,
275
- recordId: result.recordId,
276
- revision: result.previousRevision,
277
- outcome: "removed",
278
- hooks: noted.hooks,
279
- });
280
- return { scope, written: true, outcome: "updated", result, precondition: body.precondition };
281
- }
282
- if (!body.record) {
283
- throw new ServiceError("VALIDATION", "record required", 400);
284
- }
285
- const record = {
286
- ...body.record,
287
- objectType: body.objectType,
288
- contentType: body.contentType,
289
- };
290
- if (record._system && typeof record._system === "object") {
291
- const system = { ...record._system };
292
- delete system._datasource;
293
- record._system = system;
294
- }
295
- let result;
296
- let hooks;
297
- const objectType = body.objectType;
298
- const contentType = body.contentType;
299
- const recordId = body.record.recordId;
300
- if (op === "add" || precondition.form === "absent") {
301
- const noted = await runHookTrigger(() => ports.writer.add({
302
- scope,
303
- precondition: { kind: "absent" },
304
- record,
305
- datasource,
306
- suppressHooks,
307
- }));
308
- result = noted.value;
309
- hooks = noted.hooks;
310
- }
311
- else if (op === "patch") {
312
- const noted = await runHookTrigger(() => ports.writer.patch({
313
- scope,
314
- precondition: { kind: "revision", revision: Number(expectedRevision) },
315
- objectType,
316
- contentType,
317
- recordId,
318
- ops: body.patch ?? [],
319
- datasource,
320
- suppressHooks,
321
- }));
322
- result = noted.value;
323
- hooks = noted.hooks;
324
- }
325
- else if (op === "replace") {
326
- const noted = await runHookTrigger(() => ports.writer.replace({
327
- scope,
328
- precondition: { kind: "revision", revision: Number(expectedRevision) },
329
- record,
330
- datasource,
331
- suppressHooks,
332
- }));
333
- result = noted.value;
334
- hooks = noted.hooks;
335
- }
336
- else {
337
- // exact-revision upsert — never silently treat as create
338
- const noted = await runHookTrigger(() => ports.writer.upsert({
339
- scope,
340
- precondition: { kind: "revision", revision: Number(expectedRevision) },
341
- record,
342
- datasource,
343
- suppressHooks,
344
- }));
345
- result = noted.value;
346
- hooks = noted.hooks;
347
- }
348
- const indexing = await maybeEnqueueNexumAnalysis({
349
- platform,
350
- scope,
351
- objectType: body.objectType,
352
- contentType: body.contentType,
353
- recordId: result.record.recordId,
354
- revision: result.record.revision,
355
- data: result.record.data &&
356
- typeof result.record.data === "object" &&
357
- !Array.isArray(result.record.data)
358
- ? result.record.data
359
- : body.record?.data &&
360
- typeof body.record.data === "object" &&
361
- !Array.isArray(body.record.data)
362
- ? body.record.data
363
- : undefined,
364
- });
365
- memorixActivityRing.recordLand({
366
- orgId: scope.orgId,
367
- caller: getRequestCaller(req),
368
- objectType: body.objectType,
369
- contentType: body.contentType,
370
- recordId: result.record.recordId,
371
- revision: result.record.revision,
372
- outcome: result.created ? "created" : "updated",
373
- indexing,
374
- hooks,
375
- });
376
- return {
377
- scope,
378
- written: true,
379
- outcome: result.created ? "created" : "updated",
380
- result,
381
- currentRevision: result.record.revision,
382
- precondition: body.precondition,
383
- indexing,
384
- };
385
- }
386
- catch (err) {
387
- const msg = err instanceof Error ? err.message : String(err);
388
- if (/stale|revision|WRITE_STALE/i.test(msg)) {
389
- throw new ServiceError("REVISION_CONFLICT", msg, 409);
390
- }
391
- throw err;
392
- }
365
+ return { scope, receipts: rows.map(publicWriteReceipt) };
366
+ });
367
+ app.get("/api/data/receipts/:receiptId", async (req) => {
368
+ requireCallbackAccess(req, "explore");
369
+ const scope = resolveScope(req);
370
+ const caller = requireCaller(req);
371
+ const receiptId = req.params.receiptId?.trim() ?? "";
372
+ if (!receiptId)
373
+ throw new ServiceError("VALIDATION", "receiptId required", 400, { field: "receiptId" });
374
+ const row = await platform.writeReceiptsFor(scope.orgId).get(scope.orgId, receiptId);
375
+ if (!row || row.caller !== caller) {
376
+ throw new ServiceError("NOT_FOUND", "receipt not found", 404, { field: "receiptId", retryable: false });
377
+ }
378
+ return publicWriteReceipt(row);
379
+ });
380
+ app.post("/api/data/content", async (req, reply) => {
381
+ requireDeploymentAccess(req, "write");
382
+ const scope = resolveScope(req);
383
+ const body = (req.body ?? {});
384
+ await platform.analytics?.prepare(scope.orgId);
385
+ const caller = requireCaller(req);
386
+ const outcome = await runContentWrite({
387
+ scope,
388
+ caller,
389
+ datasource: contentDatasource(req, body),
390
+ ports: platform.portsFor(scope),
391
+ receipts: platform.writeReceiptsFor(scope.orgId),
392
+ onCommitted: (event) => commitWriteEffects({ platform, scope, caller, event }),
393
+ checkWrite: patchGuardCheck(platform, scope),
394
+ }, body);
395
+ if (outcome.kind === "rejected")
396
+ throw outcome.error;
397
+ if (outcome.kind === "pending")
398
+ return reply.code(202).send(outcome.body);
399
+ return outcome.body;
393
400
  });
394
401
  app.post("/api/data/content/move-fields", async (req) => {
395
402
  requireDeploymentAccess(req, "write");
@@ -540,15 +547,37 @@ export async function registerDataRoutes(app, platform) {
540
547
  pageInfo: recordId ? undefined : { nextCursor, limit },
541
548
  };
542
549
  });
550
+ app.get("/api/data/subjects/*", async (req) => {
551
+ requireCallbackAccess(req, "explore");
552
+ const scope = resolveScope(req);
553
+ const recordId = subjectRecordIdFromRequest(req);
554
+ const ports = platform.portsFor(scope);
555
+ const reader = ports.reader;
556
+ if (typeof reader.getSubject !== "function") {
557
+ throw new ServiceError("CANONICAL_PORT_MISSING", "Subject catalog requires @x12i/memorix-data with getSubject", 501);
558
+ }
559
+ const entry = await reader.getSubject(scope, recordId);
560
+ if (!entry) {
561
+ throw new ServiceError("READ_NOT_FOUND", `Subject ${recordId} is not reserved`, 404);
562
+ }
563
+ return {
564
+ scope,
565
+ recordId: entry.recordId,
566
+ objectType: entry.objectType,
567
+ dataCategory: entry.dataCategory,
568
+ contentTypes: entry.contentTypes,
569
+ };
570
+ });
543
571
  app.post("/api/data/compose", async (req) => {
544
572
  requireCallbackAccess(req, "explore");
545
573
  const scope = resolveScope(req);
546
574
  const body = parseComposeBody(req.body);
547
575
  const ports = platform.portsFor(scope);
548
- const composition = await ports.reader.composeSubject(scope, {
576
+ const includeMarkdown = body.include?.markdown === true;
577
+ const composition = (await ports.reader.composeSubject(scope, {
549
578
  recordId: body.recordId,
550
579
  contentTypes: body.contentTypes,
551
- });
580
+ }, includeMarkdown ? { include: { markdown: true } } : undefined));
552
581
  // Flatten sibling content instances for operator UI — every instance must match subject.
553
582
  const instances = [];
554
583
  for (const [contentType, list] of Object.entries(composition.contents ?? {})) {
@@ -561,6 +590,7 @@ export async function registerDataRoutes(app, platform) {
561
590
  dataCategory: composition.dataCategory,
562
591
  revision: rec.revision ?? null,
563
592
  data: rec.data ?? null,
593
+ ...(includeMarkdown && rec._markdown !== undefined ? { _markdown: rec._markdown } : {}),
564
594
  });
565
595
  }
566
596
  }
@@ -727,6 +757,8 @@ export async function registerDataRoutes(app, platform) {
727
757
  if (!body.leadingContentType?.trim()) {
728
758
  throw new ServiceError("VALIDATION", "leadingContentType required", 400);
729
759
  }
760
+ const include = parseIncludeObject(body.include);
761
+ const includeMarkdown = include?.markdown === true;
730
762
  const limit = Math.min(Math.max(Number(body.limit ?? 50) || 50, 1), 200);
731
763
  const filters = parseSafeFiltersValue(body.filter);
732
764
  const sort = parseSafeSortValue(body.sort);
@@ -739,11 +771,20 @@ export async function registerDataRoutes(app, platform) {
739
771
  sort,
740
772
  limit,
741
773
  cursor: body.cursor,
774
+ ...(includeMarkdown ? { include: { markdown: true } } : {}),
742
775
  });
743
776
  const items = listed.items.map((composition) => {
744
777
  const contents = {};
745
778
  for (const [contentType, list] of Object.entries(composition.contents ?? {})) {
746
- contents[contentType] = list.map((rec) => rec.data ?? null);
779
+ contents[contentType] = list.map((rec) => {
780
+ if (!includeMarkdown)
781
+ return rec.data ?? null;
782
+ return {
783
+ data: rec.data ?? null,
784
+ revision: rec.revision ?? null,
785
+ ...(rec._markdown !== undefined ? { _markdown: rec._markdown } : {}),
786
+ };
787
+ });
747
788
  }
748
789
  return {
749
790
  recordId: composition.recordId,
@@ -802,6 +843,7 @@ function parseComposeBody(raw) {
802
843
  return {
803
844
  recordId: body.recordId,
804
845
  contentTypes: body.contentTypes,
846
+ include: parseIncludeObject(body.include),
805
847
  };
806
848
  }
807
849
  function assertComposeInstanceRecordId(subjectId, rec) {