@deepfates/lync 0.3.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 (136) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +374 -0
  3. package/bin/lync.js +2 -0
  4. package/dist/cli/bin.d.ts +3 -0
  5. package/dist/cli/bin.d.ts.map +1 -0
  6. package/dist/cli/bin.js +5 -0
  7. package/dist/cli/bin.js.map +1 -0
  8. package/dist/cli/index.d.ts +11 -0
  9. package/dist/cli/index.d.ts.map +1 -0
  10. package/dist/cli/index.js +404 -0
  11. package/dist/cli/index.js.map +1 -0
  12. package/dist/cli/sync.d.ts +42 -0
  13. package/dist/cli/sync.d.ts.map +1 -0
  14. package/dist/cli/sync.js +232 -0
  15. package/dist/cli/sync.js.map +1 -0
  16. package/dist/client/create.d.ts +10 -0
  17. package/dist/client/create.d.ts.map +1 -0
  18. package/dist/client/create.js +50 -0
  19. package/dist/client/create.js.map +1 -0
  20. package/dist/client/index.d.ts +3 -0
  21. package/dist/client/index.d.ts.map +1 -0
  22. package/dist/client/index.js +3 -0
  23. package/dist/client/index.js.map +1 -0
  24. package/dist/client/testing.d.ts +14 -0
  25. package/dist/client/testing.d.ts.map +1 -0
  26. package/dist/client/testing.js +19 -0
  27. package/dist/client/testing.js.map +1 -0
  28. package/dist/client/types.d.ts +49 -0
  29. package/dist/client/types.d.ts.map +1 -0
  30. package/dist/client/types.js +2 -0
  31. package/dist/client/types.js.map +1 -0
  32. package/dist/errors.d.ts +16 -0
  33. package/dist/errors.d.ts.map +1 -0
  34. package/dist/errors.js +39 -0
  35. package/dist/errors.js.map +1 -0
  36. package/dist/events.d.ts +119 -0
  37. package/dist/events.d.ts.map +1 -0
  38. package/dist/events.js +686 -0
  39. package/dist/events.js.map +1 -0
  40. package/dist/file-log.d.ts +25 -0
  41. package/dist/file-log.d.ts.map +1 -0
  42. package/dist/file-log.js +97 -0
  43. package/dist/file-log.js.map +1 -0
  44. package/dist/idb-log.d.ts +21 -0
  45. package/dist/idb-log.d.ts.map +1 -0
  46. package/dist/idb-log.js +114 -0
  47. package/dist/idb-log.js.map +1 -0
  48. package/dist/index.d.ts +9 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +11 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/indexes/entries.d.ts +6 -0
  53. package/dist/indexes/entries.d.ts.map +1 -0
  54. package/dist/indexes/entries.js +7 -0
  55. package/dist/indexes/entries.js.map +1 -0
  56. package/dist/indexes/index.d.ts +3 -0
  57. package/dist/indexes/index.d.ts.map +1 -0
  58. package/dist/indexes/index.js +3 -0
  59. package/dist/indexes/index.js.map +1 -0
  60. package/dist/indexes/memory.d.ts +4 -0
  61. package/dist/indexes/memory.d.ts.map +1 -0
  62. package/dist/indexes/memory.js +193 -0
  63. package/dist/indexes/memory.js.map +1 -0
  64. package/dist/indexes/types.d.ts +70 -0
  65. package/dist/indexes/types.d.ts.map +1 -0
  66. package/dist/indexes/types.js +2 -0
  67. package/dist/indexes/types.js.map +1 -0
  68. package/dist/json.d.ts +3 -0
  69. package/dist/json.d.ts.map +1 -0
  70. package/dist/json.js +15 -0
  71. package/dist/json.js.map +1 -0
  72. package/dist/looms.d.ts +25 -0
  73. package/dist/looms.d.ts.map +1 -0
  74. package/dist/looms.js +359 -0
  75. package/dist/looms.js.map +1 -0
  76. package/dist/memory-log.d.ts +5 -0
  77. package/dist/memory-log.d.ts.map +1 -0
  78. package/dist/memory-log.js +7 -0
  79. package/dist/memory-log.js.map +1 -0
  80. package/dist/memory.d.ts +4 -0
  81. package/dist/memory.d.ts.map +1 -0
  82. package/dist/memory.js +255 -0
  83. package/dist/memory.js.map +1 -0
  84. package/dist/profiles/text-story.d.ts +26 -0
  85. package/dist/profiles/text-story.d.ts.map +1 -0
  86. package/dist/profiles/text-story.js +55 -0
  87. package/dist/profiles/text-story.js.map +1 -0
  88. package/dist/references.d.ts +14 -0
  89. package/dist/references.d.ts.map +1 -0
  90. package/dist/references.js +109 -0
  91. package/dist/references.js.map +1 -0
  92. package/dist/relay/attach.d.ts +24 -0
  93. package/dist/relay/attach.d.ts.map +1 -0
  94. package/dist/relay/attach.js +54 -0
  95. package/dist/relay/attach.js.map +1 -0
  96. package/dist/relay/index.d.ts +4 -0
  97. package/dist/relay/index.d.ts.map +1 -0
  98. package/dist/relay/index.js +4 -0
  99. package/dist/relay/index.js.map +1 -0
  100. package/dist/relay/relay.d.ts +80 -0
  101. package/dist/relay/relay.d.ts.map +1 -0
  102. package/dist/relay/relay.js +329 -0
  103. package/dist/relay/relay.js.map +1 -0
  104. package/dist/relay/serve.d.ts +18 -0
  105. package/dist/relay/serve.d.ts.map +1 -0
  106. package/dist/relay/serve.js +37 -0
  107. package/dist/relay/serve.js.map +1 -0
  108. package/dist/sha256.d.ts +2 -0
  109. package/dist/sha256.d.ts.map +1 -0
  110. package/dist/sha256.js +84 -0
  111. package/dist/sha256.js.map +1 -0
  112. package/dist/store.d.ts +97 -0
  113. package/dist/store.d.ts.map +1 -0
  114. package/dist/store.js +250 -0
  115. package/dist/store.js.map +1 -0
  116. package/dist/sync-protocol.d.ts +80 -0
  117. package/dist/sync-protocol.d.ts.map +1 -0
  118. package/dist/sync-protocol.js +135 -0
  119. package/dist/sync-protocol.js.map +1 -0
  120. package/dist/synced-store.d.ts +68 -0
  121. package/dist/synced-store.d.ts.map +1 -0
  122. package/dist/synced-store.js +340 -0
  123. package/dist/synced-store.js.map +1 -0
  124. package/dist/types.d.ts +78 -0
  125. package/dist/types.d.ts.map +1 -0
  126. package/dist/types.js +2 -0
  127. package/dist/types.js.map +1 -0
  128. package/dist/uuid.d.ts +9 -0
  129. package/dist/uuid.d.ts.map +1 -0
  130. package/dist/uuid.js +25 -0
  131. package/dist/uuid.js.map +1 -0
  132. package/dist/views.d.ts +78 -0
  133. package/dist/views.d.ts.map +1 -0
  134. package/dist/views.js +192 -0
  135. package/dist/views.js.map +1 -0
  136. package/package.json +183 -0
package/dist/events.js ADDED
@@ -0,0 +1,686 @@
1
+ import { sha256Hex } from "./sha256.js";
2
+ const topLevelFields = new Set([
3
+ "v",
4
+ "id",
5
+ "kind",
6
+ "at",
7
+ "author",
8
+ "parents",
9
+ "payload",
10
+ "marked",
11
+ "critical",
12
+ ]);
13
+ const authorFields = new Set(["actor", "operator", "via", "imported_by", "source"]);
14
+ const textDecoder = new TextDecoder("utf-8", { fatal: true });
15
+ const textEncoder = new TextEncoder();
16
+ export class LyncUnion {
17
+ pendingLimit;
18
+ lines = [];
19
+ acceptedById = new Map();
20
+ conflictIds = new Set();
21
+ conflictVariants = new Map();
22
+ pendingByParent = new Map();
23
+ pendingCount = 0;
24
+ pendingOverflowCount = 0;
25
+ constructor(options = {}) {
26
+ this.pendingLimit = options.pendingLimit ?? 1024;
27
+ }
28
+ union(input) {
29
+ const results = [];
30
+ for (const raw of parsePhysicalLines(input.file, input.bytes)) {
31
+ const line = parseLine(raw);
32
+ this.lines.push(line);
33
+ results.push(this.ingestParsedLine(line));
34
+ }
35
+ return results;
36
+ }
37
+ result() {
38
+ return buildResult(this.lines, this.acceptedById, this.conflictIds, [...this.conflictVariants.values()], this.pending(), this.pendingOverflowCount);
39
+ }
40
+ ingestParsedLine(line) {
41
+ if (line.class === "damaged")
42
+ return { status: "damaged", line };
43
+ if (line.class === "garbage")
44
+ return { status: "garbage", line };
45
+ if (!isUnionCandidate(line))
46
+ return { status: "garbage", line };
47
+ const missingParent = firstMissingParent(line, this.acceptedById, this.conflictIds);
48
+ if (missingParent) {
49
+ this.bufferPending(missingParent, line);
50
+ return { status: "buffered", line, missingParent };
51
+ }
52
+ return this.acceptLine(line);
53
+ }
54
+ acceptLine(line) {
55
+ const existing = this.acceptedById.get(line.id);
56
+ if (this.conflictIds.has(line.id)) {
57
+ return { status: "conflict", line, conflictVariants: [this.markConflictVariant(line)] };
58
+ }
59
+ if (!existing) {
60
+ this.acceptedById.set(line.id, line);
61
+ const drained = this.drain(line.id);
62
+ return drained.length ? { status: "added", line, drained } : { status: "added", line };
63
+ }
64
+ if (sameBody(existing, line)) {
65
+ line.duplicateSighting = true;
66
+ if ((line.digest ?? "") !== (existing.digest ?? "") || (line.sig ?? "") !== (existing.sig ?? "")) {
67
+ line.metadataDisagreement = true;
68
+ }
69
+ if (isRicherLine(line, existing))
70
+ this.acceptedById.set(line.id, line);
71
+ return { status: "duplicate", line };
72
+ }
73
+ this.acceptedById.delete(line.id);
74
+ this.conflictIds.add(line.id);
75
+ const variants = new Map();
76
+ for (const variantLine of this.lines.filter((variant) => variant.id === line.id && isUnionCandidate(variant))) {
77
+ const variant = this.markConflictVariant(variantLine);
78
+ variants.set(`${variant.id}\0${variant.digest}`, variant);
79
+ }
80
+ const drained = this.drain(line.id);
81
+ const conflictVariants = [...variants.values()];
82
+ return drained.length ? { status: "conflict", line, conflictVariants, drained } : { status: "conflict", line, conflictVariants };
83
+ }
84
+ markConflictVariant(line) {
85
+ line.class = "conflict-variant";
86
+ line.reason = "same id with different body bytes";
87
+ const variant = conflictVariantFor(line);
88
+ this.conflictVariants.set(`${variant.id}\0${variant.digest}`, variant);
89
+ return variant;
90
+ }
91
+ bufferPending(missingParent, line) {
92
+ const bucket = this.pendingByParent.get(missingParent) ?? [];
93
+ bucket.push(line);
94
+ this.pendingByParent.set(missingParent, bucket);
95
+ this.pendingCount++;
96
+ // In-memory union keeps pending diagnostics even past this limit; durable bounds belong to the storage layer.
97
+ if (this.pendingCount > this.pendingLimit)
98
+ this.pendingOverflowCount++;
99
+ }
100
+ drain(parent) {
101
+ const bucket = this.pendingByParent.get(parent);
102
+ if (!bucket)
103
+ return [];
104
+ this.pendingByParent.delete(parent);
105
+ this.pendingCount -= bucket.length;
106
+ const results = [];
107
+ for (const line of bucket)
108
+ results.push(this.ingestParsedLine(line));
109
+ return results;
110
+ }
111
+ pending() {
112
+ return [...this.pendingByParent.entries()].flatMap(([missingParent, lines]) => lines.map((line) => ({
113
+ missingParent,
114
+ digest: line.bodyDigest,
115
+ file: line.file,
116
+ line: line.line,
117
+ id: line.id,
118
+ bytes: line.bytes,
119
+ })));
120
+ }
121
+ }
122
+ export function parseLyncFiles(inputs) {
123
+ const lines = inputs.flatMap((input) => parsePhysicalLines(input.file, input.bytes).map(parseLine));
124
+ const { acceptedById, conflictIds, conflictVariants } = markConflicts(lines);
125
+ return buildResult(lines, acceptedById, conflictIds, conflictVariants, [], 0);
126
+ }
127
+ export function exportCarriedLyncBytes(result) {
128
+ const chunks = [];
129
+ let size = 0;
130
+ for (const line of result.lines) {
131
+ chunks.push(line.bytes);
132
+ size += line.bytes.byteLength;
133
+ if (line.terminator) {
134
+ const lf = textEncoder.encode(line.terminator);
135
+ chunks.push(lf);
136
+ size += lf.byteLength;
137
+ }
138
+ }
139
+ const out = new Uint8Array(size);
140
+ let offset = 0;
141
+ for (const chunk of chunks) {
142
+ out.set(chunk, offset);
143
+ offset += chunk.byteLength;
144
+ }
145
+ return out;
146
+ }
147
+ export function lyncDownset(result, id) {
148
+ const events = new Map();
149
+ const conflicts = new Set(result.conflictIds);
150
+ for (const line of result.lines) {
151
+ if ((line.class === "accepted" || line.class === "nonconforming") && line.id) {
152
+ events.set(line.id, line);
153
+ }
154
+ }
155
+ const ids = new Set();
156
+ const obstacles = [];
157
+ const stack = [{ id, path: [] }];
158
+ const seen = new Set();
159
+ while (stack.length) {
160
+ const current = stack.pop();
161
+ if (conflicts.has(current.id)) {
162
+ obstacles.push({ class: "unavailable-due-to-conflict", id: current.id });
163
+ continue;
164
+ }
165
+ const line = events.get(current.id);
166
+ if (!line?.event) {
167
+ obstacles.push({ class: "dangling", missing: current.id });
168
+ continue;
169
+ }
170
+ ids.add(current.id);
171
+ if (current.path.includes(current.id)) {
172
+ const start = current.path.indexOf(current.id);
173
+ obstacles.push({ class: "cycle", ids: current.path.slice(start) });
174
+ continue;
175
+ }
176
+ if (seen.has(current.id))
177
+ continue;
178
+ seen.add(current.id);
179
+ for (const parent of line.event.parents) {
180
+ stack.push({ id: parent, path: [...current.path, current.id] });
181
+ }
182
+ }
183
+ return { ids: [...ids].sort(), partial: obstacles.length > 0, obstacles: normalizeObstacles(obstacles) };
184
+ }
185
+ function parsePhysicalLines(file, bytesOrString) {
186
+ const bytes = typeof bytesOrString === "string" ? textEncoder.encode(bytesOrString) : bytesOrString;
187
+ const lines = [];
188
+ let start = 0;
189
+ let line = 1;
190
+ for (let i = 0; i < bytes.byteLength; i++) {
191
+ if (bytes[i] === 0x0a) {
192
+ lines.push({ file, line, bytes: bytes.slice(start, i), terminator: "\n" });
193
+ start = i + 1;
194
+ line++;
195
+ }
196
+ }
197
+ if (start < bytes.byteLength) {
198
+ lines.push({ file, line, bytes: bytes.slice(start), terminator: "" });
199
+ }
200
+ return lines;
201
+ }
202
+ function parseLine(raw) {
203
+ const base = { file: raw.file, line: raw.line, bytes: raw.bytes, terminator: raw.terminator };
204
+ const spliced = splitSplice(raw.bytes);
205
+ if (spliced.digest && sha256Hex(spliced.bodyBytes) !== spliced.digest.slice("sha256:".length)) {
206
+ return { ...base, class: "damaged", reason: "sha256 mismatch", bodyBytes: spliced.bodyBytes, bodyDigest: sha256Hex(spliced.bodyBytes), digest: spliced.digest, sig: spliced.sig };
207
+ }
208
+ let parsed;
209
+ try {
210
+ parsed = parseJsonNoDuplicateKeys(decodeUtf8(spliced.bodyBytes));
211
+ }
212
+ catch (error) {
213
+ return { ...base, class: "garbage", reason: errorMessage(error), bodyBytes: spliced.bodyBytes, bodyDigest: sha256Hex(spliced.bodyBytes), digest: spliced.digest, sig: spliced.sig };
214
+ }
215
+ const checked = validateEnvelope(parsed.value);
216
+ if (!checked.ok) {
217
+ return { ...base, class: "garbage", reason: checked.reason, bodyBytes: spliced.bodyBytes, bodyDigest: sha256Hex(spliced.bodyBytes), digest: spliced.digest, sig: spliced.sig };
218
+ }
219
+ const nonconformingReasons = checked.nonconforming;
220
+ if (!raw.terminator)
221
+ nonconformingReasons.push("final line missing LF");
222
+ return {
223
+ ...base,
224
+ class: nonconformingReasons.length ? "nonconforming" : "accepted",
225
+ reason: nonconformingReasons.join("; ") || "accepted",
226
+ id: checked.event.id,
227
+ event: checked.event,
228
+ bodyBytes: spliced.bodyBytes,
229
+ bodyDigest: sha256Hex(spliced.bodyBytes),
230
+ digest: spliced.digest,
231
+ sig: spliced.sig,
232
+ hasDigest: Boolean(spliced.digest),
233
+ hasSig: Boolean(spliced.sig),
234
+ nonconformingReasons,
235
+ };
236
+ }
237
+ function splitSplice(bytes) {
238
+ const text = latin1Decode(bytes);
239
+ const match = text.match(/,"digest":"(sha256:[0-9a-f]{64})"(?:,"sig":"([A-Za-z0-9+/]+={0,2})")?}$/);
240
+ if (!match)
241
+ return { bodyBytes: bytes };
242
+ const sig = match[2];
243
+ if (sig !== undefined && (sig.length === 0 || sig.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(sig) || /=.+[^=]/.test(sig))) {
244
+ return { bodyBytes: bytes };
245
+ }
246
+ const bodyBytes = new Uint8Array(match.index + 1);
247
+ bodyBytes.set(bytes.slice(0, match.index));
248
+ bodyBytes[bodyBytes.byteLength - 1] = 0x7d;
249
+ return { bodyBytes, digest: match[1], sig };
250
+ }
251
+ function latin1Decode(bytes) {
252
+ let text = "";
253
+ const chunkSize = 0x8000;
254
+ for (let offset = 0; offset < bytes.byteLength; offset += chunkSize) {
255
+ text += String.fromCharCode(...bytes.subarray(offset, offset + chunkSize));
256
+ }
257
+ return text;
258
+ }
259
+ function markConflicts(lines) {
260
+ const byId = new Map();
261
+ for (const line of lines) {
262
+ if (isUnionCandidate(line)) {
263
+ const bucket = byId.get(line.id) ?? [];
264
+ bucket.push(line);
265
+ byId.set(line.id, bucket);
266
+ }
267
+ }
268
+ const acceptedById = new Map();
269
+ const conflictIds = new Set();
270
+ const conflictVariants = new Map();
271
+ for (const bucket of byId.values()) {
272
+ const bodies = new Set(bucket.map((line) => line.bodyDigest));
273
+ if (bodies.size > 1) {
274
+ for (const line of bucket) {
275
+ line.class = "conflict-variant";
276
+ line.reason = "same id with different body bytes";
277
+ conflictIds.add(line.id);
278
+ const variant = conflictVariantFor(line);
279
+ conflictVariants.set(`${variant.id}\0${variant.digest}`, variant);
280
+ }
281
+ continue;
282
+ }
283
+ let kept = bucket[0];
284
+ for (let i = 1; i < bucket.length; i++) {
285
+ bucket[i].duplicateSighting = true;
286
+ if ((bucket[i].digest ?? "") !== (kept.digest ?? "") || (bucket[i].sig ?? "") !== (kept.sig ?? "")) {
287
+ bucket[i].metadataDisagreement = true;
288
+ }
289
+ if (isRicherLine(bucket[i], kept))
290
+ kept = bucket[i];
291
+ }
292
+ acceptedById.set(kept.id, kept);
293
+ }
294
+ return { acceptedById, conflictIds, conflictVariants: [...conflictVariants.values()] };
295
+ }
296
+ function validateEnvelope(value) {
297
+ if (!isRecord(value))
298
+ return { ok: false, reason: "top-level JSON value is not an object" };
299
+ if ("digest" in value || "sig" in value)
300
+ return { ok: false, reason: "reserved top-level digest/sig body member" };
301
+ if (value.v !== 1)
302
+ return { ok: false, reason: "unimplemented v" };
303
+ if (typeof value.id !== "string")
304
+ return { ok: false, reason: "id must be string" };
305
+ if (typeof value.kind !== "string")
306
+ return { ok: false, reason: "kind must be string" };
307
+ const slash = value.kind.indexOf("/");
308
+ if (slash <= 0 || slash === value.kind.length - 1)
309
+ return { ok: false, reason: "kind requires namespace/name slash" };
310
+ if (typeof value.at !== "string" || !isRfc3339(value.at))
311
+ return { ok: false, reason: "at fails RFC3339 ABNF" };
312
+ if ("marked" in value && (typeof value.marked !== "string" || !isRfc3339(value.marked))) {
313
+ return { ok: false, reason: "marked fails RFC3339 ABNF" };
314
+ }
315
+ if (!isRecord(value.author))
316
+ return { ok: false, reason: "author must be object" };
317
+ if (typeof value.author.actor !== "string" || value.author.actor.length === 0) {
318
+ return { ok: false, reason: "author.actor must be non-empty string" };
319
+ }
320
+ for (const field of ["operator", "via", "imported_by", "source"]) {
321
+ if (field in value.author && typeof value.author[field] !== "string") {
322
+ return { ok: false, reason: `author.${field} must be string` };
323
+ }
324
+ }
325
+ if (!Array.isArray(value.parents) || !value.parents.every((parent) => typeof parent === "string")) {
326
+ return { ok: false, reason: "parents must be list of strings" };
327
+ }
328
+ if (!isRecord(value.payload))
329
+ return { ok: false, reason: "payload must be object" };
330
+ if ("critical" in value && typeof value.critical !== "boolean")
331
+ return { ok: false, reason: "critical must be bool" };
332
+ const nonconforming = [];
333
+ for (const key of Object.keys(value)) {
334
+ if (!topLevelFields.has(key))
335
+ nonconforming.push(`unknown top-level field ${key}`);
336
+ }
337
+ for (const key of Object.keys(value.author)) {
338
+ if (!authorFields.has(key))
339
+ nonconforming.push(`unknown author field ${key}`);
340
+ }
341
+ return { ok: true, event: value, nonconforming };
342
+ }
343
+ function buildResult(lines, acceptedById, conflictIds, conflictVariants, pending, pendingOverflowCount) {
344
+ const viewEligibleIds = [...acceptedById.keys()].filter((id) => !conflictIds.has(id)).sort();
345
+ const graphDiagnostics = graphObstacles(acceptedById, conflictIds);
346
+ const suppression = computeSuppression(acceptedById, conflictIds);
347
+ return {
348
+ lines,
349
+ unionEventIds: viewEligibleIds,
350
+ viewEligibleIds,
351
+ conflictIds: [...conflictIds].sort(),
352
+ conflictVariants: conflictVariants.sort((a, b) => compareStrings(`${a.id}\0${a.digest}`, `${b.id}\0${b.digest}`)),
353
+ pending: pending.sort((a, b) => compareStrings(`${a.missingParent}\0${a.digest}`, `${b.missingParent}\0${b.digest}`)),
354
+ pendingOverflowCount,
355
+ suppression,
356
+ graphDiagnostics,
357
+ };
358
+ }
359
+ function isUnionCandidate(line) {
360
+ return (line.class === "accepted" || line.class === "nonconforming") && Boolean(line.id && line.event && line.bodyBytes && line.bodyDigest);
361
+ }
362
+ function sameBody(a, b) {
363
+ return a.bodyDigest === b.bodyDigest && bytesEqual(a.bodyBytes, b.bodyBytes);
364
+ }
365
+ function isRicherLine(candidate, current) {
366
+ if (Boolean(candidate.sig) !== Boolean(current.sig))
367
+ return Boolean(candidate.sig);
368
+ if (Boolean(candidate.digest) !== Boolean(current.digest))
369
+ return Boolean(candidate.digest);
370
+ return false;
371
+ }
372
+ function conflictVariantFor(line) {
373
+ if (!line.id || !line.event || !line.bodyBytes || !line.bodyDigest) {
374
+ throw new Error("conflict variant missing parsed event body");
375
+ }
376
+ return {
377
+ id: line.id,
378
+ digest: line.bodyDigest,
379
+ file: line.file,
380
+ line: line.line,
381
+ bytes: line.bytes,
382
+ bodyBytes: line.bodyBytes,
383
+ event: line.event,
384
+ };
385
+ }
386
+ function firstMissingParent(line, acceptedById, conflictIds) {
387
+ const parent = line.event.parents[0];
388
+ if (!parent)
389
+ return undefined;
390
+ if (acceptedById.has(parent))
391
+ return undefined;
392
+ if (conflictIds.has(parent))
393
+ return undefined;
394
+ return parent;
395
+ }
396
+ function computeSuppression(acceptedById, conflictIds) {
397
+ const suppressed = new Set();
398
+ const dangling = new Set();
399
+ const eventIds = new Set([...acceptedById.keys()].filter((id) => !conflictIds.has(id)));
400
+ for (const line of acceptedById.values()) {
401
+ const event = line.event;
402
+ if (line.class !== "accepted" || !event || event.critical !== true || conflictIds.has(event.id))
403
+ continue;
404
+ const authorNames = names(event);
405
+ for (const parent of event.parents) {
406
+ const target = acceptedById.get(parent)?.event;
407
+ if (!target || conflictIds.has(parent)) {
408
+ dangling.add(parent);
409
+ continue;
410
+ }
411
+ if (intersects(authorNames, names(target)))
412
+ suppressed.add(parent);
413
+ }
414
+ }
415
+ return {
416
+ suppressedPayloadIds: [...suppressed].sort(),
417
+ notSuppressedIds: [...eventIds].filter((id) => !suppressed.has(id)).sort(),
418
+ danglingTargetNoEffectUntilUnion: [...dangling].sort(),
419
+ };
420
+ }
421
+ function graphObstacles(acceptedById, conflictIds) {
422
+ const obstacles = [];
423
+ for (const line of acceptedById.values()) {
424
+ const event = line.event;
425
+ if (!event || conflictIds.has(event.id))
426
+ continue;
427
+ for (const parent of event.parents) {
428
+ if (conflictIds.has(parent))
429
+ obstacles.push({ class: "unavailable-due-to-conflict", id: parent });
430
+ else if (!acceptedById.has(parent))
431
+ obstacles.push({ class: "dangling", missing: parent });
432
+ }
433
+ }
434
+ for (const cycle of findCycles(acceptedById, conflictIds)) {
435
+ obstacles.push({ class: "cycle", ids: cycle });
436
+ }
437
+ return normalizeObstacles(obstacles);
438
+ }
439
+ /**
440
+ * Every distinct parent-cycle among accepted events, each reported once in a
441
+ * canonical rotation (smallest id first). One shared three-color DFS over the
442
+ * whole graph — iterative (no recursion depth limit) and O(events + parent
443
+ * edges), so a 100k-deep chain costs one walk, not one walk per event.
444
+ */
445
+ function findCycles(acceptedById, conflictIds) {
446
+ const GRAY = 1;
447
+ const BLACK = 2;
448
+ const color = new Map();
449
+ const cycles = [];
450
+ const seenCycles = new Set();
451
+ for (const startId of acceptedById.keys()) {
452
+ if (conflictIds.has(startId) || color.get(startId) === BLACK)
453
+ continue;
454
+ const stack = [{ id: startId, nextParent: 0 }];
455
+ const path = [startId];
456
+ const pathIndex = new Map([[startId, 0]]);
457
+ color.set(startId, GRAY);
458
+ while (stack.length > 0) {
459
+ const frame = stack[stack.length - 1];
460
+ const event = acceptedById.get(frame.id)?.event;
461
+ const parents = event ? event.parents : [];
462
+ if (frame.nextParent < parents.length) {
463
+ const parent = parents[frame.nextParent];
464
+ frame.nextParent += 1;
465
+ if (conflictIds.has(parent) || !acceptedById.has(parent))
466
+ continue;
467
+ const parentColor = color.get(parent);
468
+ if (parentColor === GRAY) {
469
+ const cycle = path.slice(pathIndex.get(parent));
470
+ const smallest = cycle.indexOf([...cycle].sort()[0]);
471
+ const canonical = [...cycle.slice(smallest), ...cycle.slice(0, smallest)];
472
+ const key = canonical.join("");
473
+ if (!seenCycles.has(key)) {
474
+ seenCycles.add(key);
475
+ cycles.push(canonical);
476
+ }
477
+ }
478
+ else if (parentColor !== BLACK) {
479
+ color.set(parent, GRAY);
480
+ pathIndex.set(parent, path.length);
481
+ path.push(parent);
482
+ stack.push({ id: parent, nextParent: 0 });
483
+ }
484
+ }
485
+ else {
486
+ stack.pop();
487
+ color.set(frame.id, BLACK);
488
+ path.pop();
489
+ pathIndex.delete(frame.id);
490
+ }
491
+ }
492
+ }
493
+ return cycles;
494
+ }
495
+ function normalizeObstacles(obstacles) {
496
+ const seen = new Set();
497
+ const out = [];
498
+ for (const obstacle of obstacles) {
499
+ const key = JSON.stringify(obstacle);
500
+ if (seen.has(key))
501
+ continue;
502
+ seen.add(key);
503
+ out.push(obstacle);
504
+ }
505
+ return out;
506
+ }
507
+ function names(event) {
508
+ return new Set([event.author.actor, event.author.operator, event.author.imported_by].filter((v) => typeof v === "string" && v.length > 0));
509
+ }
510
+ function intersects(a, b) {
511
+ for (const item of a)
512
+ if (b.has(item))
513
+ return true;
514
+ return false;
515
+ }
516
+ function isRfc3339(value) {
517
+ const match = value.match(/^(\d{4})-(\d{2})-(\d{2})[Tt](\d{2}):(\d{2}):(\d{2})(\.\d+)?([Zz]|[+-](\d{2}):(\d{2}))$/);
518
+ if (!match)
519
+ return false;
520
+ const [, , monthText, dayText, hourText, minuteText, secondText, , zone, offHourText, offMinuteText] = match;
521
+ const month = Number(monthText);
522
+ const day = Number(dayText);
523
+ const hour = Number(hourText);
524
+ const minute = Number(minuteText);
525
+ const second = Number(secondText);
526
+ if (month < 1 || month > 12 || day < 1 || day > 31)
527
+ return false;
528
+ if (hour > 23 || minute > 59 || second > 60)
529
+ return false;
530
+ if (zone !== "Z" && zone !== "z" && (Number(offHourText) > 23 || Number(offMinuteText) > 59))
531
+ return false;
532
+ return true;
533
+ }
534
+ function parseJsonNoDuplicateKeys(text) {
535
+ let pos = 0;
536
+ const parseValue = () => {
537
+ skipWs();
538
+ const ch = text[pos];
539
+ if (ch === "{")
540
+ return parseObject();
541
+ if (ch === "[")
542
+ return parseArray();
543
+ if (ch === '"')
544
+ return parseString();
545
+ if (ch === "-" || (ch >= "0" && ch <= "9"))
546
+ return parseNumber();
547
+ if (text.startsWith("true", pos))
548
+ return advanceLiteral("true", true);
549
+ if (text.startsWith("false", pos))
550
+ return advanceLiteral("false", false);
551
+ if (text.startsWith("null", pos))
552
+ return advanceLiteral("null", null);
553
+ throw new SyntaxError("invalid JSON");
554
+ };
555
+ const parseObject = () => {
556
+ pos++;
557
+ const obj = {};
558
+ const keys = new Set();
559
+ skipWs();
560
+ if (text[pos] === "}") {
561
+ pos++;
562
+ return obj;
563
+ }
564
+ while (true) {
565
+ skipWs();
566
+ if (text[pos] !== '"')
567
+ throw new SyntaxError("object member name must be string");
568
+ const key = parseString();
569
+ if (keys.has(key))
570
+ throw new SyntaxError("duplicate member name");
571
+ keys.add(key);
572
+ skipWs();
573
+ if (text[pos++] !== ":")
574
+ throw new SyntaxError("expected colon");
575
+ obj[key] = parseValue();
576
+ skipWs();
577
+ const ch = text[pos++];
578
+ if (ch === "}")
579
+ return obj;
580
+ if (ch !== ",")
581
+ throw new SyntaxError("expected comma");
582
+ }
583
+ };
584
+ const parseArray = () => {
585
+ pos++;
586
+ const arr = [];
587
+ skipWs();
588
+ if (text[pos] === "]") {
589
+ pos++;
590
+ return arr;
591
+ }
592
+ while (true) {
593
+ arr.push(parseValue());
594
+ skipWs();
595
+ const ch = text[pos++];
596
+ if (ch === "]")
597
+ return arr;
598
+ if (ch !== ",")
599
+ throw new SyntaxError("expected comma");
600
+ }
601
+ };
602
+ const parseString = () => {
603
+ const start = pos;
604
+ pos++;
605
+ while (pos < text.length) {
606
+ const ch = text[pos++];
607
+ if (ch === "\\") {
608
+ pos++;
609
+ continue;
610
+ }
611
+ if (ch === '"')
612
+ return JSON.parse(text.slice(start, pos));
613
+ if (ch < " ")
614
+ throw new SyntaxError("unescaped control character in string");
615
+ }
616
+ throw new SyntaxError("unterminated string");
617
+ };
618
+ const parseNumber = () => {
619
+ const start = pos;
620
+ if (text[pos] === "-")
621
+ pos++;
622
+ if (text[pos] === "0")
623
+ pos++;
624
+ else if (text[pos] >= "1" && text[pos] <= "9")
625
+ while (text[pos] >= "0" && text[pos] <= "9")
626
+ pos++;
627
+ else
628
+ throw new SyntaxError("invalid number");
629
+ if (text[pos] === ".") {
630
+ pos++;
631
+ if (!(text[pos] >= "0" && text[pos] <= "9"))
632
+ throw new SyntaxError("invalid number");
633
+ while (text[pos] >= "0" && text[pos] <= "9")
634
+ pos++;
635
+ }
636
+ if (text[pos] === "e" || text[pos] === "E") {
637
+ pos++;
638
+ if (text[pos] === "+" || text[pos] === "-")
639
+ pos++;
640
+ if (!(text[pos] >= "0" && text[pos] <= "9"))
641
+ throw new SyntaxError("invalid number");
642
+ while (text[pos] >= "0" && text[pos] <= "9")
643
+ pos++;
644
+ }
645
+ return Number(text.slice(start, pos));
646
+ };
647
+ const advanceLiteral = (literal, value) => {
648
+ pos += literal.length;
649
+ return value;
650
+ };
651
+ const skipWs = () => {
652
+ while (text[pos] === " " || text[pos] === "\n" || text[pos] === "\r" || text[pos] === "\t")
653
+ pos++;
654
+ };
655
+ if (text[0] !== "{")
656
+ throw new SyntaxError("bytes outside object or top-level is not object");
657
+ const value = parseValue();
658
+ if (pos !== text.length)
659
+ throw new SyntaxError("bytes outside object");
660
+ return { value };
661
+ }
662
+ function bytesEqual(a, b) {
663
+ if (!a || !b || a.byteLength !== b.byteLength)
664
+ return false;
665
+ for (let i = 0; i < a.byteLength; i++)
666
+ if (a[i] !== b[i])
667
+ return false;
668
+ return true;
669
+ }
670
+ function compareStrings(a, b) {
671
+ if (a < b)
672
+ return -1;
673
+ if (a > b)
674
+ return 1;
675
+ return 0;
676
+ }
677
+ function decodeUtf8(bytes) {
678
+ return textDecoder.decode(bytes);
679
+ }
680
+ function isRecord(value) {
681
+ return typeof value === "object" && value !== null && !Array.isArray(value);
682
+ }
683
+ function errorMessage(error) {
684
+ return error instanceof Error ? error.message : String(error);
685
+ }
686
+ //# sourceMappingURL=events.js.map