@pegma/knowledge-base-application 0.0.0-stage → 0.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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +140 -2
  3. package/dist/articles.d.ts +10 -0
  4. package/dist/articles.d.ts.map +1 -0
  5. package/dist/articles.js +454 -0
  6. package/dist/articles.js.map +1 -0
  7. package/dist/errors.d.ts +18 -0
  8. package/dist/errors.d.ts.map +1 -0
  9. package/dist/errors.js +34 -0
  10. package/dist/errors.js.map +1 -0
  11. package/dist/governance.d.ts +14 -0
  12. package/dist/governance.d.ts.map +1 -0
  13. package/dist/governance.js +364 -0
  14. package/dist/governance.js.map +1 -0
  15. package/dist/index.d.ts +13 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +46 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/lexical.d.ts +13 -0
  20. package/dist/lexical.d.ts.map +1 -0
  21. package/dist/lexical.js +105 -0
  22. package/dist/lexical.js.map +1 -0
  23. package/dist/publication.d.ts +42 -0
  24. package/dist/publication.d.ts.map +1 -0
  25. package/dist/publication.js +258 -0
  26. package/dist/publication.js.map +1 -0
  27. package/dist/reads.d.ts +5 -0
  28. package/dist/reads.d.ts.map +1 -0
  29. package/dist/reads.js +228 -0
  30. package/dist/reads.js.map +1 -0
  31. package/dist/records.d.ts +40 -0
  32. package/dist/records.d.ts.map +1 -0
  33. package/dist/records.js +122 -0
  34. package/dist/records.js.map +1 -0
  35. package/dist/repository.d.ts +74 -0
  36. package/dist/repository.d.ts.map +1 -0
  37. package/dist/repository.js +738 -0
  38. package/dist/repository.js.map +1 -0
  39. package/dist/retrieval-budget.d.ts +37 -0
  40. package/dist/retrieval-budget.d.ts.map +1 -0
  41. package/dist/retrieval-budget.js +111 -0
  42. package/dist/retrieval-budget.js.map +1 -0
  43. package/dist/retrieval-limits.d.ts +4 -0
  44. package/dist/retrieval-limits.d.ts.map +1 -0
  45. package/dist/retrieval-limits.js +21 -0
  46. package/dist/retrieval-limits.js.map +1 -0
  47. package/dist/retrieval-maintenance.d.ts +19 -0
  48. package/dist/retrieval-maintenance.d.ts.map +1 -0
  49. package/dist/retrieval-maintenance.js +359 -0
  50. package/dist/retrieval-maintenance.js.map +1 -0
  51. package/dist/retrieval-records.d.ts +55 -0
  52. package/dist/retrieval-records.d.ts.map +1 -0
  53. package/dist/retrieval-records.js +140 -0
  54. package/dist/retrieval-records.js.map +1 -0
  55. package/dist/retrieval-repository.d.ts +32 -0
  56. package/dist/retrieval-repository.d.ts.map +1 -0
  57. package/dist/retrieval-repository.js +750 -0
  58. package/dist/retrieval-repository.js.map +1 -0
  59. package/dist/retrieval-types.d.ts +185 -0
  60. package/dist/retrieval-types.d.ts.map +1 -0
  61. package/dist/retrieval-types.js +24 -0
  62. package/dist/retrieval-types.js.map +1 -0
  63. package/dist/retrieval.d.ts +12 -0
  64. package/dist/retrieval.d.ts.map +1 -0
  65. package/dist/retrieval.js +541 -0
  66. package/dist/retrieval.js.map +1 -0
  67. package/dist/storage-helpers.d.ts +23 -0
  68. package/dist/storage-helpers.d.ts.map +1 -0
  69. package/dist/storage-helpers.js +138 -0
  70. package/dist/storage-helpers.js.map +1 -0
  71. package/dist/types.d.ts +280 -0
  72. package/dist/types.d.ts.map +1 -0
  73. package/dist/types.js +2 -0
  74. package/dist/types.js.map +1 -0
  75. package/package.json +42 -4
@@ -0,0 +1,738 @@
1
+ import { auditRecordId, defineAudit, } from "@pegma/audit";
2
+ import { knowledgeRecords, encodeKnowledgeRecord, maximumTransactionActions, maximumTransactionEncodedBytes, } from "./records.js";
3
+ import { canonicalJson, encodedBytes, fingerprint, RepositoryCapacityError, RepositoryConflictError, RepositoryIntegrityError, requireGeneration, requireIdentifier, snapshotData, } from "./storage-helpers.js";
4
+ export const defaultKnowledgeRepositoryLimits = Object.freeze({
5
+ maxPhysicalRows: 1_024,
6
+ maxRetainedBytes: 16 * 1024 * 1024,
7
+ controlReserveRows: 32,
8
+ controlReserveBytes: 512 * 1024,
9
+ maxArticleRevisions: 64,
10
+ maxEvidenceVersions: 256,
11
+ maxConflictAttempts: 3,
12
+ });
13
+ function limitsFor(overrides) {
14
+ const names = Object.keys(defaultKnowledgeRepositoryLimits);
15
+ if (Object.keys(overrides).some((name) => !names.includes(name)))
16
+ throw new TypeError("Unknown knowledge repository limit");
17
+ const limits = { ...defaultKnowledgeRepositoryLimits, ...overrides };
18
+ for (const [name, value] of Object.entries(limits)) {
19
+ const minimum = name.startsWith("controlReserve") ? 0 : 1;
20
+ if (!Number.isSafeInteger(value) || value < minimum)
21
+ throw new TypeError("Knowledge repository limits must be bounded integers");
22
+ }
23
+ if (limits.maxPhysicalRows > 10_000 ||
24
+ limits.maxRetainedBytes > 64 * 1024 * 1024 ||
25
+ limits.maxArticleRevisions > 1_000 ||
26
+ limits.maxEvidenceVersions > 1_000 ||
27
+ limits.maxConflictAttempts > 8 ||
28
+ limits.controlReserveRows >= limits.maxPhysicalRows ||
29
+ limits.controlReserveBytes >= limits.maxRetainedBytes)
30
+ throw new TypeError("Knowledge repository limits exceed their ceilings");
31
+ return Object.freeze(limits);
32
+ }
33
+ export function createKnowledgeRepository(store, overrides = {}) {
34
+ const limits = limitsFor(snapshotData(overrides));
35
+ const records = store.collection(knowledgeRecords);
36
+ const partition = async (kind, id) => {
37
+ requireIdentifier(id);
38
+ return kind + ":" + (await fingerprint([kind, id]));
39
+ };
40
+ const evidenceId = async (revision, id, version) => {
41
+ requireGeneration(revision, 1);
42
+ requireGeneration(version, 1);
43
+ requireIdentifier(id);
44
+ return ("evidence:" + revision + ":" + (await fingerprint(id)) + ":" + version);
45
+ };
46
+ function checkIdentity(row, kind, id, scope, key) {
47
+ if (row.resourceKind !== kind ||
48
+ row.resourceId !== id ||
49
+ row.partition !== scope ||
50
+ row.id !== key)
51
+ throw new RepositoryIntegrityError();
52
+ }
53
+ function validatePointer(pointer) {
54
+ try {
55
+ requireGeneration(pointer.generation, 1);
56
+ if (pointer.resourceKind === "article") {
57
+ requireGeneration(pointer.current, 1);
58
+ requireGeneration(pointer.provenance);
59
+ if (!["draft", "in-review", "published", "retired"].includes(pointer.state) ||
60
+ typeof pointer.publicationHeld !== "boolean" ||
61
+ !Array.isArray(pointer.evidence) ||
62
+ pointer.evidence.length > 32 ||
63
+ !Array.isArray(pointer.revocations) ||
64
+ pointer.revocations.length > limits.maxEvidenceVersions)
65
+ throw new RepositoryIntegrityError();
66
+ for (const evidence of pointer.evidence) {
67
+ requireIdentifier(evidence.id);
68
+ requireGeneration(evidence.articleRevision, 1);
69
+ requireGeneration(evidence.version, 1);
70
+ if (evidence.articleRevision !== pointer.current ||
71
+ !/^[a-f0-9]{64}$/u.test(evidence.hash))
72
+ throw new RepositoryIntegrityError();
73
+ }
74
+ if (new Set(pointer.revocations).size !== pointer.revocations.length ||
75
+ pointer.revocations.some((hash) => typeof hash !== "string" || !/^[a-f0-9]{64}$/u.test(hash)))
76
+ throw new RepositoryIntegrityError();
77
+ for (const key of [
78
+ pointer.review,
79
+ pointer.publicationReview,
80
+ pointer.published?.approval,
81
+ ]) {
82
+ if (key !== undefined &&
83
+ (typeof key !== "string" ||
84
+ key.length > 300 ||
85
+ /[^A-Za-z0-9|:._-]/u.test(key)))
86
+ throw new RepositoryIntegrityError();
87
+ }
88
+ if (pointer.published !== undefined) {
89
+ requireGeneration(pointer.published.revision, 1);
90
+ if (pointer.publishedProvenance === undefined ||
91
+ pointer.publicationReview === undefined)
92
+ throw new RepositoryIntegrityError();
93
+ }
94
+ else if (pointer.publishedProvenance !== undefined ||
95
+ pointer.publicationReview !== undefined ||
96
+ pointer.state === "published")
97
+ throw new RepositoryIntegrityError();
98
+ if (pointer.publishedProvenance !== undefined)
99
+ requireGeneration(pointer.publishedProvenance);
100
+ }
101
+ else if (pointer.resourceKind === "collection") {
102
+ requireGeneration(pointer.revision, 1);
103
+ if (pointer.configuration !== undefined &&
104
+ (typeof pointer.configuration !== "string" ||
105
+ !/^configuration:[0-9]+$/u.test(pointer.configuration)))
106
+ throw new RepositoryIntegrityError();
107
+ }
108
+ else if (pointer.resourceKind === "validation")
109
+ requireGeneration(pointer.version, 1);
110
+ else
111
+ throw new RepositoryIntegrityError();
112
+ }
113
+ catch {
114
+ throw new RepositoryIntegrityError();
115
+ }
116
+ }
117
+ async function getRow(kind, id, key) {
118
+ if (typeof key !== "string" ||
119
+ key.length === 0 ||
120
+ key.length > 300 ||
121
+ /[^A-Za-z0-9|:._-]/u.test(key))
122
+ throw new RepositoryIntegrityError();
123
+ const scope = await partition(kind, id);
124
+ const row = await records.get({ partition: scope, id: key });
125
+ if (row !== null)
126
+ checkIdentity(row, kind, id, scope, key);
127
+ return row;
128
+ }
129
+ async function required(kind, id, key, expectedKind) {
130
+ const row = await getRow(kind, id, key);
131
+ if (row === null || row.kind !== expectedKind)
132
+ throw new RepositoryIntegrityError();
133
+ return row.data;
134
+ }
135
+ async function getHead(kind, id) {
136
+ const scope = await partition(kind, id);
137
+ const row = await records.getVersioned({ partition: scope, id: "head" });
138
+ if (row === null)
139
+ return null;
140
+ checkIdentity(row.value, kind, id, scope, "head");
141
+ try {
142
+ if (row.value.kind !== "head")
143
+ throw new RepositoryIntegrityError();
144
+ const data = row.value.data;
145
+ if (data?.pointer?.resourceKind !== kind || data.quota === undefined)
146
+ throw new RepositoryIntegrityError();
147
+ requireGeneration(data.pointer.generation, 1);
148
+ validatePointer(data.pointer);
149
+ if (canonicalJson(Object.keys(data.quota).sort()) !==
150
+ canonicalJson(["bytes", "evidence", "revisions", "rows"]))
151
+ throw new RepositoryIntegrityError();
152
+ for (const count of Object.values(data.quota)) {
153
+ if (!Number.isSafeInteger(count) || count < 0)
154
+ throw new RepositoryIntegrityError();
155
+ }
156
+ if (data.quota.rows < 1 ||
157
+ data.quota.rows > limits.maxPhysicalRows ||
158
+ data.quota.bytes > limits.maxRetainedBytes)
159
+ throw new RepositoryCapacityError("stored_partition");
160
+ const allowed = kind === "article"
161
+ ? [
162
+ "resourceKind",
163
+ "generation",
164
+ "state",
165
+ "current",
166
+ "evidence",
167
+ "provenance",
168
+ "review",
169
+ "published",
170
+ "publishedProvenance",
171
+ "publicationReview",
172
+ "publicationHeld",
173
+ "revocations",
174
+ ]
175
+ : kind === "collection"
176
+ ? ["resourceKind", "generation", "revision", "configuration"]
177
+ : ["resourceKind", "generation", "version"];
178
+ if (Object.keys(data).some((key) => !["pointer", "quota"].includes(key)) ||
179
+ Object.keys(data.pointer).some((key) => !allowed.includes(key)))
180
+ throw new RepositoryIntegrityError();
181
+ return row;
182
+ }
183
+ catch (error) {
184
+ if (error instanceof RepositoryCapacityError ||
185
+ error instanceof RepositoryIntegrityError)
186
+ throw error;
187
+ throw new RepositoryIntegrityError();
188
+ }
189
+ }
190
+ async function getRevision(id, number) {
191
+ requireGeneration(number, 1);
192
+ const row = await getRow("article", id, "revision:" + number);
193
+ if (row === null)
194
+ return null;
195
+ const value = row.data;
196
+ if (row.kind !== "revision" ||
197
+ value?.articleId !== id ||
198
+ value.number !== number)
199
+ throw new RepositoryIntegrityError();
200
+ return value;
201
+ }
202
+ async function getEvidence(id, revision, identifier, version) {
203
+ const row = await getRow("article", id, await evidenceId(revision, identifier, version));
204
+ if (row === null)
205
+ return null;
206
+ const value = row.data;
207
+ if (row.kind !== "evidence" ||
208
+ value?.articleId !== id ||
209
+ value.articleRevision !== revision ||
210
+ value.id !== identifier ||
211
+ value.version !== version)
212
+ throw new RepositoryIntegrityError();
213
+ return value;
214
+ }
215
+ async function hydrate(kind, id, pointer) {
216
+ if (pointer.resourceKind !== kind)
217
+ throw new RepositoryIntegrityError();
218
+ validatePointer(pointer);
219
+ if (pointer.resourceKind === "collection") {
220
+ const collection = await required(kind, id, "collection-policy:" + pointer.revision, "collection-policy");
221
+ if (collection.id !== id || collection.revision !== pointer.revision)
222
+ throw new RepositoryIntegrityError();
223
+ const configuration = pointer.configuration === undefined
224
+ ? undefined
225
+ : await required(kind, id, pointer.configuration, "configuration");
226
+ return {
227
+ generation: pointer.generation,
228
+ collection,
229
+ ...(configuration === undefined ? {} : { configuration }),
230
+ };
231
+ }
232
+ if (pointer.resourceKind === "validation") {
233
+ const policy = await required(kind, id, "policy:" + pointer.version, "policy");
234
+ if (policy.id !== id || policy.version !== pointer.version)
235
+ throw new RepositoryIntegrityError();
236
+ return { generation: pointer.generation, policy };
237
+ }
238
+ requireGeneration(pointer.current, 1);
239
+ if (!Array.isArray(pointer.evidence) ||
240
+ pointer.evidence.length > 32 ||
241
+ !Array.isArray(pointer.revocations) ||
242
+ pointer.revocations.length > limits.maxEvidenceVersions ||
243
+ typeof pointer.publicationHeld !== "boolean")
244
+ throw new RepositoryIntegrityError();
245
+ const current = await getRevision(id, pointer.current);
246
+ if (current === null)
247
+ throw new RepositoryIntegrityError();
248
+ const evidence = await Promise.all(pointer.evidence.map(async (reference) => {
249
+ if (reference.articleRevision !== pointer.current)
250
+ throw new RepositoryIntegrityError();
251
+ const value = await getEvidence(id, reference.articleRevision, reference.id, reference.version);
252
+ if (value === null || value.hash !== reference.hash)
253
+ throw new RepositoryIntegrityError();
254
+ return value;
255
+ }));
256
+ const provenance = await required(kind, id, "provenance:" + pointer.provenance, "provenance");
257
+ const review = pointer.review === undefined
258
+ ? undefined
259
+ : await required(kind, id, pointer.review, "review");
260
+ let published;
261
+ if (pointer.published !== undefined) {
262
+ const revision = await getRevision(id, pointer.published.revision);
263
+ const approval = await required(kind, id, pointer.published.approval, "publication");
264
+ if (revision === null ||
265
+ approval.binding.articleId !== id ||
266
+ approval.binding.articleRevision !== revision.number ||
267
+ approval.binding.contentHash !== revision.contentHash)
268
+ throw new RepositoryIntegrityError();
269
+ published = { revision, approval };
270
+ }
271
+ const publishedProvenance = pointer.publishedProvenance === undefined
272
+ ? undefined
273
+ : await required(kind, id, "provenance:" + pointer.publishedProvenance, "provenance");
274
+ const publicationReview = pointer.publicationReview === undefined
275
+ ? undefined
276
+ : await required(kind, id, pointer.publicationReview, "review");
277
+ const revokedEvidence = await Promise.all(pointer.revocations.map((hash) => required(kind, id, "revoked:" + hash, "revocation")));
278
+ const article = {
279
+ id,
280
+ state: pointer.state,
281
+ current,
282
+ evidence,
283
+ ...(review === undefined ? {} : { review: review.binding }),
284
+ ...(published === undefined ? {} : { published }),
285
+ };
286
+ return {
287
+ generation: pointer.generation,
288
+ article,
289
+ provenance,
290
+ publicationHeld: pointer.publicationHeld,
291
+ revokedEvidence,
292
+ ...(review === undefined ? {} : { review }),
293
+ ...(publishedProvenance === undefined ? {} : { publishedProvenance }),
294
+ ...(publicationReview === undefined ? {} : { publicationReview }),
295
+ };
296
+ }
297
+ async function read(kind, id) {
298
+ const row = await getHead(kind, id);
299
+ if (row === null)
300
+ return null;
301
+ const head = row.value.data;
302
+ return {
303
+ value: (await hydrate(kind, id, head.pointer)),
304
+ version: row.version,
305
+ };
306
+ }
307
+ async function readPublication(id) {
308
+ const row = await getHead("article", id);
309
+ if (row === null)
310
+ return null;
311
+ const pointer = row.value.data.pointer;
312
+ const value = {
313
+ articleId: id,
314
+ generation: pointer.generation,
315
+ state: pointer.state,
316
+ publicationHeld: pointer.publicationHeld,
317
+ };
318
+ if (pointer.published === undefined)
319
+ return { value, version: row.version };
320
+ const [revision, approval, publishedProvenance, publicationReview] = await Promise.all([
321
+ getRevision(id, pointer.published.revision),
322
+ required("article", id, pointer.published.approval, "publication"),
323
+ required("article", id, "provenance:" + pointer.publishedProvenance, "provenance"),
324
+ required("article", id, pointer.publicationReview, "review"),
325
+ ]);
326
+ if (revision === null ||
327
+ approval.binding.articleId !== id ||
328
+ approval.binding.articleRevision !== revision.number ||
329
+ approval.binding.contentHash !== revision.contentHash) {
330
+ throw new RepositoryIntegrityError();
331
+ }
332
+ return {
333
+ value: {
334
+ ...value,
335
+ published: { revision, approval },
336
+ publishedProvenance,
337
+ publicationReview,
338
+ },
339
+ version: row.version,
340
+ };
341
+ }
342
+ async function execute(kind, input) {
343
+ const request = snapshotData({
344
+ id: input.id,
345
+ command: input.command,
346
+ ...(input.expectedGeneration === undefined
347
+ ? {}
348
+ : { expectedGeneration: input.expectedGeneration }),
349
+ control: input.control === true,
350
+ });
351
+ const recheck = input.recheck;
352
+ const decide = input.decide;
353
+ requireIdentifier(request.id);
354
+ for (const field of [
355
+ request.command.commandId,
356
+ request.command.correlationId,
357
+ request.command.actorId,
358
+ request.command.operation,
359
+ ])
360
+ requireIdentifier(field);
361
+ if (!/^[a-f0-9]{64}$/u.test(request.command.fingerprint))
362
+ throw new TypeError("Expected a command fingerprint");
363
+ const commandTime = Date.parse(request.command.occurredAt);
364
+ if (typeof request.command.occurredAt !== "string" ||
365
+ !Number.isFinite(commandTime) ||
366
+ new Date(commandTime).toISOString() !== request.command.occurredAt)
367
+ throw new TypeError("Expected canonical command time");
368
+ if (request.expectedGeneration !== undefined)
369
+ requireGeneration(request.expectedGeneration);
370
+ const scope = await partition(kind, request.id);
371
+ const commandHash = await fingerprint(request.command.commandId);
372
+ const receiptKey = "command:" + commandHash;
373
+ for (let attempt = 0; attempt < limits.maxConflictAttempts; attempt += 1) {
374
+ const beforeRow = await getHead(kind, request.id);
375
+ const beforeHead = beforeRow?.value.data;
376
+ const current = beforeHead === undefined
377
+ ? null
378
+ : (await hydrate(kind, request.id, beforeHead.pointer));
379
+ await recheck(current === null ? null : snapshotData(current));
380
+ const receiptRow = await getRow(kind, request.id, receiptKey);
381
+ if (receiptRow !== null) {
382
+ const receipt = receiptRow.data;
383
+ if (receiptRow.kind !== "command" ||
384
+ receipt.command.actorId !== request.command.actorId ||
385
+ receipt.command.operation !== request.command.operation ||
386
+ receipt.command.fingerprint !== request.command.fingerprint ||
387
+ receipt.command.commandId !== request.command.commandId)
388
+ throw new RepositoryConflictError();
389
+ // Receipt and head reads are not a snapshot. The matching command may
390
+ // have committed after this attempt loaded its earlier head.
391
+ const latest = await read(kind, request.id);
392
+ await recheck(latest?.value ?? null);
393
+ if (latest === null ||
394
+ receipt.result.generation > latest.value.generation)
395
+ throw new RepositoryConflictError();
396
+ const historical = (await hydrate(kind, request.id, receipt.result));
397
+ return { value: historical, replayed: true };
398
+ }
399
+ if (request.expectedGeneration !== undefined &&
400
+ request.expectedGeneration !== (current?.generation ?? 0))
401
+ throw new RepositoryConflictError();
402
+ const generation = (current?.generation ?? 0) + 1;
403
+ requireGeneration(generation, 1);
404
+ const decision = snapshotData(await decide(current === null ? null : snapshotData(current), generation));
405
+ const next = snapshotData({ ...decision.next, generation });
406
+ const additions = [];
407
+ const queued = new Map();
408
+ let immutablePlanningCollision = false;
409
+ const buildRow = (id, rowKind, data) => ({
410
+ schemaVersion: 1,
411
+ resourceKind: kind,
412
+ resourceId: request.id,
413
+ partition: scope,
414
+ id,
415
+ kind: rowKind,
416
+ data: snapshotData(data),
417
+ });
418
+ const add = async (key, rowKind, data) => {
419
+ if (immutablePlanningCollision)
420
+ return;
421
+ const record = buildRow(key, rowKind, data);
422
+ const pending = queued.get(key);
423
+ if (pending !== undefined) {
424
+ if (canonicalJson(pending) !== canonicalJson(record))
425
+ throw new RepositoryIntegrityError();
426
+ return;
427
+ }
428
+ const previous = await getRow(kind, request.id, key);
429
+ if (previous !== null) {
430
+ if (canonicalJson(previous) !== canonicalJson(record)) {
431
+ // A matching command may have committed after this attempt read
432
+ // its receipt. Re-enter the receipt-first, authorized retry path;
433
+ // server timestamps are not part of semantic command identity.
434
+ immutablePlanningCollision = true;
435
+ }
436
+ return;
437
+ }
438
+ encodeKnowledgeRecord(record);
439
+ queued.set(key, record);
440
+ additions.push(record);
441
+ };
442
+ let pointer;
443
+ if (kind === "article") {
444
+ const value = next;
445
+ const previous = current;
446
+ const previousPointer = beforeHead?.pointer;
447
+ if (value.article.id !== request.id ||
448
+ value.article.current.articleId !== request.id ||
449
+ value.article.current.number > limits.maxArticleRevisions ||
450
+ value.article.evidence.length > 32 ||
451
+ value.revokedEvidence.length > limits.maxEvidenceVersions ||
452
+ value.provenance.references.length > 32)
453
+ throw new RepositoryCapacityError("article_history");
454
+ if (typeof value.publicationHeld !== "boolean" ||
455
+ canonicalJson(value.article.review ?? null) !==
456
+ canonicalJson(value.review?.binding ?? null) ||
457
+ (value.article.published !== undefined &&
458
+ (value.publicationReview === undefined ||
459
+ value.publishedProvenance === undefined)) ||
460
+ (value.article.published === undefined &&
461
+ (value.publicationReview !== undefined ||
462
+ value.publishedProvenance !== undefined)))
463
+ throw new RepositoryIntegrityError();
464
+ await add("revision:" + value.article.current.number, "revision", value.article.current);
465
+ for (const evidence of value.article.evidence) {
466
+ if (evidence.articleId !== request.id ||
467
+ evidence.articleRevision !== value.article.current.number)
468
+ throw new RepositoryIntegrityError();
469
+ await add(await evidenceId(evidence.articleRevision, evidence.id, evidence.version), "evidence", evidence);
470
+ }
471
+ await add("provenance:" + value.provenance.generation, "provenance", value.provenance);
472
+ if (value.publishedProvenance !== undefined)
473
+ await add("provenance:" + value.publishedProvenance.generation, "provenance", value.publishedProvenance);
474
+ const reviewKey = async (review, slot) => {
475
+ if (review === undefined)
476
+ return undefined;
477
+ if (previousPointer?.review !== undefined &&
478
+ previous?.review !== undefined &&
479
+ canonicalJson(previous.review) === canonicalJson(review))
480
+ return previousPointer.review;
481
+ if (previousPointer?.publicationReview !== undefined &&
482
+ previous?.publicationReview !== undefined &&
483
+ canonicalJson(previous.publicationReview) === canonicalJson(review))
484
+ return previousPointer.publicationReview;
485
+ const key = "review:" + generation + ":" + slot;
486
+ await add(key, "review", review);
487
+ return key;
488
+ };
489
+ const review = await reviewKey(value.review, "pending");
490
+ const publicationReview = await reviewKey(value.publicationReview, "publication");
491
+ let published;
492
+ if (value.article.published !== undefined) {
493
+ await add("revision:" + value.article.published.revision.number, "revision", value.article.published.revision);
494
+ const unchanged = previous?.article.published !== undefined &&
495
+ canonicalJson(previous.article.published) ===
496
+ canonicalJson(value.article.published);
497
+ const approval = unchanged && previousPointer?.published !== undefined
498
+ ? previousPointer.published.approval
499
+ : "publication:" + generation;
500
+ await add(approval, "publication", value.article.published.approval);
501
+ published = {
502
+ revision: value.article.published.revision.number,
503
+ approval,
504
+ };
505
+ }
506
+ for (const revoked of value.revokedEvidence) {
507
+ if (!/^[a-f0-9]{64}$/u.test(revoked.hash))
508
+ throw new RepositoryIntegrityError();
509
+ await add("revoked:" + revoked.hash, "revocation", revoked);
510
+ }
511
+ for (const revoked of previous?.revokedEvidence ?? []) {
512
+ if (!value.revokedEvidence.some((candidate) => canonicalJson(candidate) === canonicalJson(revoked)))
513
+ throw new RepositoryIntegrityError();
514
+ }
515
+ pointer = {
516
+ resourceKind: "article",
517
+ generation,
518
+ state: value.article.state,
519
+ current: value.article.current.number,
520
+ evidence: value.article.evidence.map(({ id, articleRevision, version, hash }) => ({
521
+ id,
522
+ articleRevision,
523
+ version,
524
+ hash,
525
+ })),
526
+ provenance: value.provenance.generation,
527
+ publicationHeld: value.publicationHeld,
528
+ revocations: value.revokedEvidence.map(({ hash }) => hash),
529
+ ...(review === undefined ? {} : { review }),
530
+ ...(published === undefined ? {} : { published }),
531
+ ...(value.publishedProvenance === undefined
532
+ ? {}
533
+ : { publishedProvenance: value.publishedProvenance.generation }),
534
+ ...(publicationReview === undefined ? {} : { publicationReview }),
535
+ };
536
+ }
537
+ else if (kind === "collection") {
538
+ const value = next;
539
+ const previous = current;
540
+ const previousPointer = beforeHead?.pointer;
541
+ if (value.collection.id !== request.id)
542
+ throw new RepositoryIntegrityError();
543
+ await add("collection-policy:" + value.collection.revision, "collection-policy", value.collection);
544
+ let configuration;
545
+ if (value.configuration !== undefined) {
546
+ configuration =
547
+ previous?.configuration !== undefined &&
548
+ canonicalJson(previous.configuration) ===
549
+ canonicalJson(value.configuration)
550
+ ? previousPointer?.configuration
551
+ : undefined;
552
+ configuration ??= "configuration:" + generation;
553
+ await add(configuration, "configuration", value.configuration);
554
+ }
555
+ pointer = {
556
+ resourceKind: "collection",
557
+ generation,
558
+ revision: value.collection.revision,
559
+ ...(configuration === undefined ? {} : { configuration }),
560
+ };
561
+ }
562
+ else {
563
+ const value = next;
564
+ if (value.policy.id !== request.id)
565
+ throw new RepositoryIntegrityError();
566
+ await add("policy:" + value.policy.version, "policy", value.policy);
567
+ pointer = {
568
+ resourceKind: "validation",
569
+ generation,
570
+ version: value.policy.version,
571
+ };
572
+ }
573
+ if (immutablePlanningCollision)
574
+ continue;
575
+ const audit = defineAudit({
576
+ collection: knowledgeRecords,
577
+ toRecord: (event) => buildRow(auditRecordId(event.id), "audit", event),
578
+ toEvent: (record) => record.kind === "audit" ? record.data : null,
579
+ });
580
+ const event = {
581
+ id: commandHash,
582
+ occurredAt: request.command.occurredAt,
583
+ actor: { kind: "principal", principalId: request.command.actorId },
584
+ action: request.command.operation,
585
+ subject: request.id,
586
+ sequence: generation,
587
+ details: {
588
+ ...decision.auditDetails,
589
+ correlationId: request.command.correlationId,
590
+ },
591
+ };
592
+ const auditAction = audit.action(event);
593
+ if (auditAction.action !== "insert")
594
+ throw new RepositoryIntegrityError();
595
+ additions.push(auditAction.value);
596
+ additions.push(buildRow(receiptKey, "command", {
597
+ command: request.command,
598
+ result: pointer,
599
+ }));
600
+ const jobs = decision.jobs ?? [
601
+ {
602
+ task: kind === "article"
603
+ ? "article_changed"
604
+ : kind === "collection"
605
+ ? "collection_changed"
606
+ : "validation_changed",
607
+ },
608
+ ];
609
+ if (jobs.length > 8)
610
+ throw new RepositoryCapacityError("command_jobs");
611
+ for (const [index, job] of jobs.entries()) {
612
+ if (![
613
+ "article_changed",
614
+ "collection_changed",
615
+ "validation_changed",
616
+ ].includes(job.task))
617
+ throw new TypeError("Unknown knowledge task");
618
+ if (Object.keys(job.payload ?? {}).length > 32 ||
619
+ Object.values(job.payload ?? {}).some((value) => typeof value === "string" && value.length > 2_048))
620
+ throw new RepositoryCapacityError("job_payload");
621
+ additions.push(buildRow("job:" + commandHash + ":" + index, "job", {
622
+ task: job.task,
623
+ payload: job.payload ?? {},
624
+ generation,
625
+ status: "pending",
626
+ createdAt: request.command.occurredAt,
627
+ }));
628
+ }
629
+ const priorQuota = beforeHead?.quota ?? {
630
+ rows: 0,
631
+ bytes: 0,
632
+ revisions: 0,
633
+ evidence: 0,
634
+ };
635
+ const quota = {
636
+ rows: priorQuota.rows + additions.length + (beforeRow === null ? 1 : 0),
637
+ bytes: 0,
638
+ revisions: priorQuota.revisions +
639
+ additions.filter((row) => row.kind === "revision").length,
640
+ evidence: priorQuota.evidence +
641
+ additions.filter((row) => row.kind === "evidence").length,
642
+ };
643
+ if (quota.revisions > limits.maxArticleRevisions ||
644
+ quota.evidence > limits.maxEvidenceVersions)
645
+ throw new RepositoryCapacityError("retained_history");
646
+ const encodedAdditions = additions.map(encodeKnowledgeRecord);
647
+ const baseBytes = priorQuota.bytes -
648
+ (beforeRow === null
649
+ ? 0
650
+ : encodedBytes(encodeKnowledgeRecord(beforeRow.value))) +
651
+ encodedAdditions.reduce((sum, row) => sum + encodedBytes(row), 0);
652
+ let headRow = buildRow("head", "head", { pointer, quota });
653
+ for (let pass = 0; pass < 12; pass += 1) {
654
+ const updated = baseBytes + encodedBytes(encodeKnowledgeRecord(headRow));
655
+ if (updated === quota.bytes)
656
+ break;
657
+ quota.bytes = updated;
658
+ headRow = buildRow("head", "head", { pointer, quota });
659
+ }
660
+ const rowBudget = limits.maxPhysicalRows -
661
+ (request.control ? 0 : limits.controlReserveRows);
662
+ const byteBudget = limits.maxRetainedBytes -
663
+ (request.control ? 0 : limits.controlReserveBytes);
664
+ if (quota.rows > rowBudget)
665
+ throw new RepositoryCapacityError("physical_rows");
666
+ if (quota.bytes > byteBudget)
667
+ throw new RepositoryCapacityError("retained_bytes");
668
+ const actions = [
669
+ beforeRow === null
670
+ ? { action: "insert", value: headRow }
671
+ : {
672
+ action: "putIfUnchanged",
673
+ value: headRow,
674
+ version: beforeRow.version,
675
+ },
676
+ ...additions.map((value) => ({ action: "insert", value })),
677
+ ];
678
+ if (actions.length > maximumTransactionActions ||
679
+ encodedBytes(encodeKnowledgeRecord(headRow)) +
680
+ encodedAdditions.reduce((sum, row) => sum + encodedBytes(row), 0) >
681
+ maximumTransactionEncodedBytes)
682
+ throw new RepositoryCapacityError("transaction");
683
+ const outcome = await records.transact(scope, actions);
684
+ if (!outcome.committed)
685
+ continue;
686
+ const latest = await read(kind, request.id);
687
+ await recheck(latest?.value ?? null);
688
+ return { value: next, replayed: false };
689
+ }
690
+ // The winning attempt may have committed this command on the final retry.
691
+ const receiptRow = await getRow(kind, request.id, receiptKey);
692
+ if (receiptRow !== null && receiptRow.kind === "command") {
693
+ const receipt = receiptRow.data;
694
+ if (receipt.command.actorId === request.command.actorId &&
695
+ receipt.command.operation === request.command.operation &&
696
+ receipt.command.fingerprint === request.command.fingerprint &&
697
+ receipt.command.commandId === request.command.commandId) {
698
+ const latest = await read(kind, request.id);
699
+ await recheck(latest?.value ?? null);
700
+ if (latest === null ||
701
+ receipt.result.generation > latest.value.generation)
702
+ throw new RepositoryConflictError();
703
+ return {
704
+ value: (await hydrate(kind, request.id, receipt.result)),
705
+ replayed: true,
706
+ };
707
+ }
708
+ }
709
+ throw new RepositoryConflictError();
710
+ }
711
+ return {
712
+ limits,
713
+ readArticleVersioned: (id) => read("article", id),
714
+ readArticlePublicationVersioned: readPublication,
715
+ readResourceGeneration: async (kind, id) => {
716
+ const row = await getHead(kind, id);
717
+ return row === null ? null : row.value.data.pointer.generation;
718
+ },
719
+ readCollectionVersioned: (id) => read("collection", id),
720
+ readValidationPolicyVersioned: (id) => read("validation", id),
721
+ getArticleRevision: getRevision,
722
+ getEvidenceVersion: getEvidence,
723
+ isEvidenceRevoked: async (id, hash) => {
724
+ if (!/^[a-f0-9]{64}$/u.test(hash))
725
+ throw new TypeError("Expected evidence hash");
726
+ const row = await getRow("article", id, "revoked:" + hash);
727
+ if (row !== null && row.kind !== "revocation")
728
+ throw new RepositoryIntegrityError();
729
+ if (row !== null && row.data?.hash !== hash)
730
+ throw new RepositoryIntegrityError();
731
+ return row !== null;
732
+ },
733
+ executeArticle: (input) => execute("article", input),
734
+ executeCollection: (input) => execute("collection", input),
735
+ executeValidationPolicy: (input) => execute("validation", input),
736
+ };
737
+ }
738
+ //# sourceMappingURL=repository.js.map