@opendatalabs/personal-server-ts-stream-sync 0.0.0-stage → 0.0.1-canary.14eb2b2

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/README.md +73 -2
  2. package/dist/bytes.d.ts +43 -0
  3. package/dist/bytes.d.ts.map +1 -0
  4. package/dist/bytes.js +45 -0
  5. package/dist/bytes.js.map +1 -0
  6. package/dist/control.d.ts +38 -0
  7. package/dist/control.d.ts.map +1 -0
  8. package/dist/control.js +45 -0
  9. package/dist/control.js.map +1 -0
  10. package/dist/decide.d.ts +108 -0
  11. package/dist/decide.d.ts.map +1 -0
  12. package/dist/decide.js +144 -0
  13. package/dist/decide.js.map +1 -0
  14. package/dist/eligible.d.ts +67 -0
  15. package/dist/eligible.d.ts.map +1 -0
  16. package/dist/eligible.js +60 -0
  17. package/dist/eligible.js.map +1 -0
  18. package/dist/errors.d.ts +41 -0
  19. package/dist/errors.d.ts.map +1 -0
  20. package/dist/errors.js +59 -0
  21. package/dist/errors.js.map +1 -0
  22. package/dist/head.d.ts +160 -0
  23. package/dist/head.d.ts.map +1 -0
  24. package/dist/head.js +315 -0
  25. package/dist/head.js.map +1 -0
  26. package/dist/index.d.ts +18 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +18 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/json.d.ts +20 -0
  31. package/dist/json.d.ts.map +1 -0
  32. package/dist/json.js +67 -0
  33. package/dist/json.js.map +1 -0
  34. package/dist/master-key.d.ts +21 -0
  35. package/dist/master-key.d.ts.map +1 -0
  36. package/dist/master-key.js +76 -0
  37. package/dist/master-key.js.map +1 -0
  38. package/dist/merge.d.ts +70 -0
  39. package/dist/merge.d.ts.map +1 -0
  40. package/dist/merge.js +80 -0
  41. package/dist/merge.js.map +1 -0
  42. package/dist/names.d.ts +44 -0
  43. package/dist/names.d.ts.map +1 -0
  44. package/dist/names.js +105 -0
  45. package/dist/names.js.map +1 -0
  46. package/dist/namespace.d.ts +32 -0
  47. package/dist/namespace.d.ts.map +1 -0
  48. package/dist/namespace.js +51 -0
  49. package/dist/namespace.js.map +1 -0
  50. package/dist/needs-full.d.ts +34 -0
  51. package/dist/needs-full.d.ts.map +1 -0
  52. package/dist/needs-full.js +52 -0
  53. package/dist/needs-full.js.map +1 -0
  54. package/dist/outbox.d.ts +67 -0
  55. package/dist/outbox.d.ts.map +1 -0
  56. package/dist/outbox.js +57 -0
  57. package/dist/outbox.js.map +1 -0
  58. package/dist/parts.d.ts +37 -0
  59. package/dist/parts.d.ts.map +1 -0
  60. package/dist/parts.js +93 -0
  61. package/dist/parts.js.map +1 -0
  62. package/dist/planner.d.ts +56 -0
  63. package/dist/planner.d.ts.map +1 -0
  64. package/dist/planner.js +127 -0
  65. package/dist/planner.js.map +1 -0
  66. package/dist/records.d.ts +51 -0
  67. package/dist/records.d.ts.map +1 -0
  68. package/dist/records.js +113 -0
  69. package/dist/records.js.map +1 -0
  70. package/dist/vectors.d.ts +24 -0
  71. package/dist/vectors.d.ts.map +1 -0
  72. package/dist/vectors.js +289 -0
  73. package/dist/vectors.js.map +1 -0
  74. package/package.json +36 -4
  75. package/vectors/v1.json +4563 -0
package/README.md CHANGED
@@ -1,3 +1,74 @@
1
- # Temporary Holding Version
1
+ # @opendatalabs/personal-server-ts-stream-sync
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The pure core of whole-file stream sync (design revision 4.1, section 2). Every
4
+ host (full server, Lite, and client libraries) builds against this one tested
5
+ package.
6
+
7
+ The package does no I/O. It imports no SDK, no Node built-ins, no browser APIs,
8
+ no clock and no randomness. Hosts inject SHA-256, Keccak-256 and the namespace
9
+ HMAC, and do every registry, storage and spool call themselves.
10
+
11
+ This is version 0.0.1 and marked `private`. It is not wired into the release
12
+ workflows.
13
+
14
+ ## Contract
15
+
16
+ | Area | Rule |
17
+ | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
18
+ | Names | Stream scope `pdpp.<c>~<ns>.<s>`, control scope `pdpp.<c>~<ns>`. `ns` is 16 lowercase hex, or empty for identity-less connectors. Head blob `h-<keccak hex>`, part blob `g<gen>-p-<sha256 hex>`. Scopes over 256 UTF-8 bytes are a permanent error. |
19
+ | Master key | `streamMasterKeyFromSignature(signature)` returns the 65-byte owner signature as r, low s and v = 27/28. Every stream-sync key (`K_ns`, head and part keys) derives from it, so every encoding of one signature gives one `ns` and `keyCheck`. Legacy scope keys keep the raw signature. |
20
+ | Namespace | `ns = hex(first 8 bytes of HMAC-SHA256(K_ns, canonicalJson([c, accountId])))`, `keyCheck = hex(first 8 bytes of HMAC-SHA256(K_ns, "key-check"))`. The host derives `K_ns` with label `pdpp-stream-sync/namespace/v1` and passes an HMAC port bound to it. |
21
+ | Control row | `ACTIVE_DATA = keccak256("pdpp.stream-sync.connection.active.v1:" + keyCheck)`. A read classifies as `absent`, `active(g)`, `deleted(v)` or `foreign` (another key's row). |
22
+ | Lines, parts | `pkHash = sha256(canonicalJson([pk values]))`. A part is NDJSON lines `{"o":<run start ms>,"r":"<runId>","v":<record>}` sorted by `pkHash`. A line over 8 MiB is `record_too_large`. Parts ≤ 16 MiB, heads ≤ 1 MiB. |
23
+ | Merge | Greatest `(o, r)` wins per key; on equal `(o, r)` the later line wins. Feed base lines, then run lines. Seed lines have `o = 0`, `r = "seed"`. The reducer keeps one line per key. |
24
+ | Planner | 256 spool shards by first `pkHash` byte. Split a bucket over the target by the next bit, never deeper than 16 bits. Deeper than 8 bits, the host makes one counting pass per shard when asked (`needs_counts`). |
25
+ | Head format 1 | Canonical JSON with `complete`, `connector`, `format`, `generation` (decimal string), `keyCheck`, `ns`, `observedAt`, `parts`, `primaryKey`, `profile {key, version}`, `recordCount`, `stream`. Readers check the hash, format, names and `keyCheck`. |
26
+ | Admission | `decide(S, C)` over the start snapshot `S` and the authoritative read `C`. The target generation depends on `S` only, so one run never publishes at two generations. After a create or reconnect reply the host re-reads and decides with phase `after_write`. |
27
+ | Checkpoint | Save `(ns, T)` only when every stream is committed or no-op at `T`, no stream was published `complete:false`, seed receipts are in, and control re-reads `active(T)`. |
28
+ | `needsFull` | Publishing `complete:false` sets it and clears the checkpoint; a full run that commits `complete:true` clears it; any entry for a connector forces full runs. |
29
+ | Eligibility | One predicate for reads, readiness and totals: a complete live view, else `unavailable` while any connection of the connector is deleted, else a legacy or raw file, else `unavailable`. `isReady` is `eligible(...).kind !== "unavailable"`. |
30
+
31
+ Open questions and the readings the package takes until they are answered are
32
+ in [DESIGN-QUESTIONS.md](DESIGN-QUESTIONS.md).
33
+
34
+ ## API
35
+
36
+ | Export | Purpose |
37
+ | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
38
+ | `Sha256`, `Keccak256`, `NamespaceHmac`, `HashPorts` | Port types the host implements. |
39
+ | `canonicalJson` | The one serialization behind every byte and hash. |
40
+ | `streamScope`, `controlScope`, `parseScope` | Build and parse registry scopes. |
41
+ | `headBlobName`, `partBlobName`, `parseBlobName` | Build and parse blob names. |
42
+ | `deriveNs`, `deriveKeyCheck`, `controlHashes` | Namespace, key check and control-row hashes. |
43
+ | `encodeLine`, `decodeLine`, `pkHash`, `shardOf`, `pkHashBits`, `inPrefix`, `lineSize` | Line codec, key hash, shard and prefix helpers, the 8 MiB line limit. |
44
+ | `compareStamp`, `BucketReducer`, `mergeLines` | Merge rule and winner-only reduce. |
45
+ | `encodePart`, `encodeWinners`, `decodePart` | Part codec with fail-closed verification. |
46
+ | `planBuckets`, `basePartsFor` | Bucket planner and base-part routing. |
47
+ | `encodeHead`, `decodeHead`, `digestHead`, `verifyHead` | Head format 1. |
48
+ | `isHeadLive`, `chooseBase`, `writerCompatibility`, `headComplete`, `isNoop` | Head rules. |
49
+ | `classifyControl`, `sameControl` | Control-row states. |
50
+ | `startState`, `decide`, `targetFor`, `checkpointDecision` | Admission, generation and checkpoint rules. |
51
+ | `afterCommit`, `runMustBeFull` | `needsFull` rules. |
52
+ | `selectConnection`, `eligible`, `isReady` | The eligibility predicate. |
53
+ | `createOutboxEntry`, `streamRunResult`, `OutboxEntry` | Outbox entry type. |
54
+ | `runVectors` | Run `vectors/v1.json` against a host's ports. |
55
+
56
+ ## Vectors
57
+
58
+ `vectors/v1.json` holds byte-exact expected outputs (namespace, scopes, key
59
+ hashes, lines, merge, parts, planner, heads) and the expected result of every
60
+ rule (every `decide` table cell, traces, checkpoint, `needsFull`,
61
+ eligibility). It is plain JSON. A host in another repository can load it and
62
+ call `runVectors(vectors, ports)` with its own crypto ports, or read it with
63
+ its own runner.
64
+
65
+ `scripts/generate-vectors.mjs` writes the file. It computes bytes with
66
+ `node:crypto` and `@noble/hashes`, independently of the package code, and
67
+ holds the rule expectations as hand-written tables. A test fails if the
68
+ checked-in file differs from the generator output.
69
+
70
+ ## Tests
71
+
72
+ ```sh
73
+ nice -n 10 npx vitest run --maxWorkers=4 packages/stream-sync
74
+ ```
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Ports and byte helpers. The package imports no crypto, clock, randomness or
3
+ * I/O: hosts inject every hash and the namespace HMAC.
4
+ */
5
+ /** SHA-256 of the exact bytes, as the 32-byte digest. */
6
+ export type Sha256 = (data: Uint8Array) => Promise<Uint8Array>;
7
+ /** Keccak-256 (Ethereum, not SHA3-256) of the exact bytes, as the 32-byte digest. */
8
+ export type Keccak256 = (data: Uint8Array) => Promise<Uint8Array>;
9
+ /**
10
+ * HMAC-SHA256 keyed with `K_ns`, as the 32-byte digest. The host binds the key
11
+ * inside its own closure, so `K_ns` never enters this package.
12
+ */
13
+ export type NamespaceHmac = (message: Uint8Array) => Promise<Uint8Array>;
14
+ /** Hash ports used by codec functions. */
15
+ export interface HashPorts {
16
+ readonly sha256: Sha256;
17
+ readonly keccak256: Keccak256;
18
+ }
19
+ export declare function utf8(text: string): Uint8Array;
20
+ export declare function utf8Length(text: string): number;
21
+ export declare function toHex(bytes: Uint8Array): string;
22
+ /** Lowercase 64-hex SHA-256 of the bytes, no prefix. */
23
+ export declare function sha256Hex(sha256: Sha256, data: Uint8Array): Promise<string>;
24
+ /** Registry form of a Keccak-256 hash: `0x` + 64 lowercase hex. */
25
+ export declare function keccakHex(keccak256: Keccak256, data: Uint8Array): Promise<`0x${string}`>;
26
+ export declare const HEX64: RegExp;
27
+ export declare const HEX16: RegExp;
28
+ export declare const REGISTRY_HASH: RegExp;
29
+ /** §2.2: a record whose canonical line exceeds this is not published. */
30
+ export declare const RECORD_LINE_LIMIT_BYTES: number;
31
+ /** §2.2: default writer target per part. Hosts with less memory pass a smaller target. */
32
+ export declare const DEFAULT_PART_TARGET_BYTES: number;
33
+ /** §2.2: decoder limit for a clear head. */
34
+ export declare const HEAD_LIMIT_BYTES: number;
35
+ /** §2.2: decoder limit for a clear part; §2.6 bucket winners limit. */
36
+ export declare const PART_LIMIT_BYTES: number;
37
+ /** §2.1: registry scope names longer than this are a permanent error. */
38
+ export declare const NAME_LIMIT_BYTES = 256;
39
+ /** §2.6: the planner never splits deeper than this many bits. */
40
+ export declare const PLANNER_DEPTH_CAP = 16;
41
+ /** §2.6: number of spool shards (first pkHash byte). */
42
+ export declare const SHARD_COUNT = 256;
43
+ //# sourceMappingURL=bytes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bytes.d.ts","sourceRoot":"","sources":["../src/bytes.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,yDAAyD;AACzD,MAAM,MAAM,MAAM,GAAG,CAAC,IAAI,EAAE,UAAU,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;AAE/D,qFAAqF;AACrF,MAAM,MAAM,SAAS,GAAG,CAAC,IAAI,EAAE,UAAU,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;AAElE;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,UAAU,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;AAEzE,0CAA0C;AAC1C,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;CAC/B;AAID,wBAAgB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAE7C;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED,wBAAgB,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAM/C;AAED,wDAAwD;AACxD,wBAAsB,SAAS,CAC7B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,UAAU,GACf,OAAO,CAAC,MAAM,CAAC,CAEjB;AAED,mEAAmE;AACnE,wBAAsB,SAAS,CAC7B,SAAS,EAAE,SAAS,EACpB,IAAI,EAAE,UAAU,GACf,OAAO,CAAC,KAAK,MAAM,EAAE,CAAC,CAExB;AAED,eAAO,MAAM,KAAK,QAAmB,CAAC;AACtC,eAAO,MAAM,KAAK,QAAmB,CAAC;AACtC,eAAO,MAAM,aAAa,QAAqB,CAAC;AAIhD,yEAAyE;AACzE,eAAO,MAAM,uBAAuB,QAAU,CAAC;AAC/C,0FAA0F;AAC1F,eAAO,MAAM,yBAAyB,QAAU,CAAC;AACjD,4CAA4C;AAC5C,eAAO,MAAM,gBAAgB,QAAU,CAAC;AACxC,uEAAuE;AACvE,eAAO,MAAM,gBAAgB,QAAW,CAAC;AACzC,yEAAyE;AACzE,eAAO,MAAM,gBAAgB,MAAM,CAAC;AACpC,iEAAiE;AACjE,eAAO,MAAM,iBAAiB,KAAK,CAAC;AACpC,wDAAwD;AACxD,eAAO,MAAM,WAAW,MAAM,CAAC"}
package/dist/bytes.js ADDED
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Ports and byte helpers. The package imports no crypto, clock, randomness or
3
+ * I/O: hosts inject every hash and the namespace HMAC.
4
+ */
5
+ const encoder = new TextEncoder();
6
+ export function utf8(text) {
7
+ return encoder.encode(text);
8
+ }
9
+ export function utf8Length(text) {
10
+ return encoder.encode(text).length;
11
+ }
12
+ export function toHex(bytes) {
13
+ let out = "";
14
+ for (let i = 0; i < bytes.length; i += 1) {
15
+ out += bytes[i].toString(16).padStart(2, "0");
16
+ }
17
+ return out;
18
+ }
19
+ /** Lowercase 64-hex SHA-256 of the bytes, no prefix. */
20
+ export async function sha256Hex(sha256, data) {
21
+ return toHex(await sha256(data));
22
+ }
23
+ /** Registry form of a Keccak-256 hash: `0x` + 64 lowercase hex. */
24
+ export async function keccakHex(keccak256, data) {
25
+ return `0x${toHex(await keccak256(data))}`;
26
+ }
27
+ export const HEX64 = /^[0-9a-f]{64}$/;
28
+ export const HEX16 = /^[0-9a-f]{16}$/;
29
+ export const REGISTRY_HASH = /^0x[0-9a-f]{64}$/;
30
+ const MiB = 1024 * 1024;
31
+ /** §2.2: a record whose canonical line exceeds this is not published. */
32
+ export const RECORD_LINE_LIMIT_BYTES = 8 * MiB;
33
+ /** §2.2: default writer target per part. Hosts with less memory pass a smaller target. */
34
+ export const DEFAULT_PART_TARGET_BYTES = 4 * MiB;
35
+ /** §2.2: decoder limit for a clear head. */
36
+ export const HEAD_LIMIT_BYTES = 1 * MiB;
37
+ /** §2.2: decoder limit for a clear part; §2.6 bucket winners limit. */
38
+ export const PART_LIMIT_BYTES = 16 * MiB;
39
+ /** §2.1: registry scope names longer than this are a permanent error. */
40
+ export const NAME_LIMIT_BYTES = 256;
41
+ /** §2.6: the planner never splits deeper than this many bits. */
42
+ export const PLANNER_DEPTH_CAP = 16;
43
+ /** §2.6: number of spool shards (first pkHash byte). */
44
+ export const SHARD_COUNT = 256;
45
+ //# sourceMappingURL=bytes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bytes.js","sourceRoot":"","sources":["../src/bytes.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAoBH,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;AAElC,MAAM,UAAU,IAAI,CAAC,IAAY;IAC/B,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AAC9B,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC;AACrC,CAAC;AAED,MAAM,UAAU,KAAK,CAAC,KAAiB;IACrC,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACzC,GAAG,IAAI,KAAK,CAAC,CAAC,CAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACjD,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,wDAAwD;AACxD,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,MAAc,EACd,IAAgB;IAEhB,OAAO,KAAK,CAAC,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;AACnC,CAAC;AAED,mEAAmE;AACnE,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,SAAoB,EACpB,IAAgB;IAEhB,OAAO,KAAK,KAAK,CAAC,MAAM,SAAS,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;AAC7C,CAAC;AAED,MAAM,CAAC,MAAM,KAAK,GAAG,gBAAgB,CAAC;AACtC,MAAM,CAAC,MAAM,KAAK,GAAG,gBAAgB,CAAC;AACtC,MAAM,CAAC,MAAM,aAAa,GAAG,kBAAkB,CAAC;AAEhD,MAAM,GAAG,GAAG,IAAI,GAAG,IAAI,CAAC;AAExB,yEAAyE;AACzE,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,GAAG,GAAG,CAAC;AAC/C,0FAA0F;AAC1F,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,GAAG,GAAG,CAAC;AACjD,4CAA4C;AAC5C,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,GAAG,GAAG,CAAC;AACxC,uEAAuE;AACvE,MAAM,CAAC,MAAM,gBAAgB,GAAG,EAAE,GAAG,GAAG,CAAC;AACzC,yEAAyE;AACzE,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAAG,CAAC;AACpC,iEAAiE;AACjE,MAAM,CAAC,MAAM,iBAAiB,GAAG,EAAE,CAAC;AACpC,wDAAwD;AACxD,MAAM,CAAC,MAAM,WAAW,GAAG,GAAG,CAAC"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * §2.3 control-row states.
3
+ *
4
+ * The authoritative control read is `GET /v1/data/:id?includeDeleted=true`.
5
+ * A device classifies the row against its own `ACTIVE_DATA`:
6
+ *
7
+ * absent no row
8
+ * deleted(v) the row at version v is a tombstone
9
+ * active(g) live row whose dataHash is this device's ACTIVE_DATA; g = version
10
+ * foreign live row with any other dataHash (another key → key_mismatch)
11
+ */
12
+ import type { ControlHashes } from "./namespace.js";
13
+ export type ControlState = {
14
+ readonly kind: "absent";
15
+ } | {
16
+ readonly kind: "active";
17
+ readonly generation: number;
18
+ } | {
19
+ readonly kind: "deleted";
20
+ readonly version: number;
21
+ } | {
22
+ readonly kind: "foreign";
23
+ readonly version: number;
24
+ };
25
+ /** What the registry returned for the control scope. */
26
+ export type ControlRead = {
27
+ readonly found: false;
28
+ } | {
29
+ readonly found: true;
30
+ readonly version: number;
31
+ readonly dataHash: string;
32
+ readonly metadataHash: string;
33
+ /** The row's `deleted_at` is set. */
34
+ readonly deleted: boolean;
35
+ };
36
+ export declare function classifyControl(read: ControlRead, hashes: Pick<ControlHashes, "activeData" | "tombstoneData" | "tombstoneMetadata">): ControlState;
37
+ export declare function sameControl(a: ControlState, b: ControlState): boolean;
38
+ //# sourceMappingURL=control.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"control.d.ts","sourceRoot":"","sources":["../src/control.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEpD,MAAM,MAAM,YAAY,GACpB;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,GAC3B;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GACxD;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACtD;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3D,wDAAwD;AACxD,MAAM,MAAM,WAAW,GACnB;IAAE,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;CAAE,GACzB;IACE,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,qCAAqC;IACrC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B,CAAC;AAUN,wBAAgB,eAAe,CAC7B,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,IAAI,CACV,aAAa,EACb,YAAY,GAAG,eAAe,GAAG,mBAAmB,CACrD,GACA,YAAY,CAcd;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,YAAY,GAAG,OAAO,CAUrE"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * §2.3 control-row states.
3
+ *
4
+ * The authoritative control read is `GET /v1/data/:id?includeDeleted=true`.
5
+ * A device classifies the row against its own `ACTIVE_DATA`:
6
+ *
7
+ * absent no row
8
+ * deleted(v) the row at version v is a tombstone
9
+ * active(g) live row whose dataHash is this device's ACTIVE_DATA; g = version
10
+ * foreign live row with any other dataHash (another key → key_mismatch)
11
+ */
12
+ import { InvalidArgumentError } from "./errors.js";
13
+ function checkVersion(version) {
14
+ if (!Number.isSafeInteger(version) || version < 1) {
15
+ throw new InvalidArgumentError("registry version must be a positive safe integer");
16
+ }
17
+ }
18
+ export function classifyControl(read, hashes) {
19
+ if (!read.found)
20
+ return { kind: "absent" };
21
+ checkVersion(read.version);
22
+ const dataHash = read.dataHash.toLowerCase();
23
+ const metadataHash = read.metadataHash.toLowerCase();
24
+ const tombstone = dataHash === hashes.tombstoneData &&
25
+ metadataHash === hashes.tombstoneMetadata;
26
+ if (read.deleted || tombstone)
27
+ return { kind: "deleted", version: read.version };
28
+ if (dataHash === hashes.activeData) {
29
+ return { kind: "active", generation: read.version };
30
+ }
31
+ return { kind: "foreign", version: read.version };
32
+ }
33
+ export function sameControl(a, b) {
34
+ if (a.kind !== b.kind)
35
+ return false;
36
+ switch (a.kind) {
37
+ case "absent":
38
+ return true;
39
+ case "active":
40
+ return a.generation === b.generation;
41
+ default:
42
+ return a.version === b.version;
43
+ }
44
+ }
45
+ //# sourceMappingURL=control.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"control.js","sourceRoot":"","sources":["../src/control.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAqBnD,SAAS,YAAY,CAAC,OAAe;IACnC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;QAClD,MAAM,IAAI,oBAAoB,CAC5B,kDAAkD,CACnD,CAAC;IACJ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,eAAe,CAC7B,IAAiB,EACjB,MAGC;IAED,IAAI,CAAC,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;IAC3C,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC3B,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC;IAC7C,MAAM,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC,WAAW,EAAE,CAAC;IACrD,MAAM,SAAS,GACb,QAAQ,KAAK,MAAM,CAAC,aAAa;QACjC,YAAY,KAAK,MAAM,CAAC,iBAAiB,CAAC;IAC5C,IAAI,IAAI,CAAC,OAAO,IAAI,SAAS;QAC3B,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC;IACpD,IAAI,QAAQ,KAAK,MAAM,CAAC,UAAU,EAAE,CAAC;QACnC,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC;IACtD,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC;AACpD,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,CAAe,EAAE,CAAe;IAC1D,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC;IACpC,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC;QACf,KAAK,QAAQ;YACX,OAAO,IAAI,CAAC;QACd,KAAK,QAAQ;YACX,OAAO,CAAC,CAAC,UAAU,KAAM,CAAc,CAAC,UAAU,CAAC;QACrD;YACE,OAAO,CAAC,CAAC,OAAO,KAAM,CAAc,CAAC,OAAO,CAAC;IACjD,CAAC;AACH,CAAC"}
@@ -0,0 +1,108 @@
1
+ /**
2
+ * §2.4 admission and generation rule.
3
+ *
4
+ * At run start the host stores `start`: a copy of the connector's control
5
+ * states from the last registry listing completed in this app session, keyed
6
+ * by ns, or `null` when no listing has completed. At publish,
7
+ *
8
+ * S = start == null ? unknown : start[ns] ?? absent
9
+ * C = the authoritative control read
10
+ *
11
+ * and `decide(S, C)` gives the action. The target generation T depends on S
12
+ * alone (absent → 1, active(g0) → g0, deleted(v0) → v0 + 1), so every stream
13
+ * of one run publishes under the same T, or the run is discarded. A spool is
14
+ * never re-bound to another generation.
15
+ *
16
+ * After a create or reconnect reply (2xx or `replayed`) the host re-reads
17
+ * the control row and calls `decide` again with phase `after_write`; only
18
+ * `publish` may follow, anything else discards. A replay proves only that the
19
+ * row was written once.
20
+ */
21
+ import type { ControlState } from "./control.js";
22
+ export type StartSnapshot = Readonly<Record<string, ControlState>> | null;
23
+ export type StartState = {
24
+ readonly kind: "unknown";
25
+ } | ControlState;
26
+ export declare function startState(start: StartSnapshot, ns: string): StartState;
27
+ /**
28
+ * `first`: first decision for this stream in this attempt.
29
+ * `reread`: the host re-read control once after a `reread` decision.
30
+ * `after_write`: the host re-read control after its create/reconnect reply.
31
+ * `stale_base`: the host re-read control after `chooseBase` returned
32
+ * `stale_generation` (a stream head of a newer generation than T).
33
+ */
34
+ export type DecidePhase = "first" | "reread" | "after_write" | "stale_base";
35
+ export interface DecideInput {
36
+ readonly start: StartState;
37
+ readonly current: ControlState;
38
+ /** True only when an explicit connect action started this run. */
39
+ readonly userConnect: boolean;
40
+ /** The run used no checkpoint. */
41
+ readonly full: boolean;
42
+ readonly phase: DecidePhase;
43
+ }
44
+ export type Decision = {
45
+ readonly kind: "publish";
46
+ readonly target: number;
47
+ }
48
+ /** POST control v1 ACTIVE (exclusive), then re-read and decide `after_write`. */
49
+ | {
50
+ readonly kind: "create";
51
+ readonly target: 1;
52
+ }
53
+ /** POST control v0+1 ACTIVE (exclusive), then re-read and decide `after_write`. */
54
+ | {
55
+ readonly kind: "reconnect";
56
+ readonly target: number;
57
+ }
58
+ /** Re-read control once and decide with phase `reread`. */
59
+ | {
60
+ readonly kind: "reread";
61
+ } | {
62
+ readonly kind: "keep_pending";
63
+ readonly reason: "registry_inconsistent" | "key_mismatch";
64
+ } | {
65
+ readonly kind: "discard";
66
+ /** Start snapshot was unknown: collect again. */
67
+ readonly recollect: boolean;
68
+ /** Clear checkpoints for (c, ns). */
69
+ readonly clearCheckpoints: boolean;
70
+ /** A userConnect run discarded only for not being full: start a full run now. */
71
+ readonly restartFull: boolean;
72
+ };
73
+ export declare function decide(input: DecideInput): Decision;
74
+ /** The target generation a start state admits, or `null` (unknown, foreign). */
75
+ export declare function targetFor(start: StartState): number | null;
76
+ /** Per-stream result of one run, as kept in the outbox. */
77
+ export type StreamRunResult = {
78
+ readonly state: "committed" | "noop";
79
+ readonly generation: number;
80
+ /** `complete` of the committed (or unchanged) head. */
81
+ readonly complete: boolean;
82
+ } | {
83
+ /** Not committed: still pending (including a visible failure) or discarded. */
84
+ readonly state: "pending" | "discarded";
85
+ };
86
+ export type CheckpointDecision = {
87
+ readonly kind: "save";
88
+ readonly generation: number;
89
+ } | {
90
+ readonly kind: "skip";
91
+ readonly reason: "stream_not_committed" | "published_incomplete" | "seed_receipts_pending" | "control_changed";
92
+ };
93
+ /**
94
+ * §2.4 checkpoint save: after every stream of the run is committed or no-op
95
+ * at T, re-read control. Save tagged (ns, T) only if it is `active(T)` and
96
+ * no stream was published `complete:false` or left a `complete:false` head
97
+ * unchanged (no-op, ruling Q9). A `record_too_large` exclusion
98
+ * does not block; a `bucket_too_large` stream is not committed and does. §2.9:
99
+ * the checkpoint also waits for every seed receipt of the run (a host merging its retained history).
100
+ */
101
+ export declare function checkpointDecision(input: {
102
+ readonly target: number;
103
+ readonly streams: Readonly<Record<string, StreamRunResult>>;
104
+ /** Control re-read after the last commit. */
105
+ readonly control: ControlState;
106
+ readonly seedReceiptsPending: boolean;
107
+ }): CheckpointDecision;
108
+ //# sourceMappingURL=decide.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decide.d.ts","sourceRoot":"","sources":["../src/decide.ts"],"names":[],"mappings":"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"}
package/dist/decide.js ADDED
@@ -0,0 +1,144 @@
1
+ /**
2
+ * §2.4 admission and generation rule.
3
+ *
4
+ * At run start the host stores `start`: a copy of the connector's control
5
+ * states from the last registry listing completed in this app session, keyed
6
+ * by ns, or `null` when no listing has completed. At publish,
7
+ *
8
+ * S = start == null ? unknown : start[ns] ?? absent
9
+ * C = the authoritative control read
10
+ *
11
+ * and `decide(S, C)` gives the action. The target generation T depends on S
12
+ * alone (absent → 1, active(g0) → g0, deleted(v0) → v0 + 1), so every stream
13
+ * of one run publishes under the same T, or the run is discarded. A spool is
14
+ * never re-bound to another generation.
15
+ *
16
+ * After a create or reconnect reply (2xx or `replayed`) the host re-reads
17
+ * the control row and calls `decide` again with phase `after_write`; only
18
+ * `publish` may follow, anything else discards. A replay proves only that the
19
+ * row was written once.
20
+ */
21
+ import { hasOwn } from "./json.js";
22
+ export function startState(start, ns) {
23
+ if (start === null)
24
+ return { kind: "unknown" };
25
+ return hasOwn(start, ns) ? start[ns] : { kind: "absent" };
26
+ }
27
+ const DISCARD = {
28
+ kind: "discard",
29
+ recollect: false,
30
+ clearCheckpoints: false,
31
+ restartFull: false,
32
+ };
33
+ function table(input) {
34
+ const { start: s, current: c, userConnect, full, phase } = input;
35
+ // `any | foreign` overrides the `other` cells (see DESIGN-QUESTIONS.md Q1).
36
+ if (c.kind === "foreign")
37
+ return { kind: "keep_pending", reason: "key_mismatch" };
38
+ switch (s.kind) {
39
+ case "unknown":
40
+ return { ...DISCARD, recollect: true };
41
+ case "absent":
42
+ if (c.kind === "absent")
43
+ return { kind: "create", target: 1 };
44
+ if (c.kind === "active" && c.generation === 1) {
45
+ return { kind: "publish", target: 1 };
46
+ }
47
+ return DISCARD;
48
+ case "active": {
49
+ const g0 = s.generation;
50
+ if (c.kind === "active" && c.generation === g0) {
51
+ return { kind: "publish", target: g0 };
52
+ }
53
+ if (c.kind === "absent" || (c.kind === "active" && c.generation < g0)) {
54
+ return phase === "reread"
55
+ ? { kind: "keep_pending", reason: "registry_inconsistent" }
56
+ : { kind: "reread" };
57
+ }
58
+ return { ...DISCARD, clearCheckpoints: true };
59
+ }
60
+ case "deleted": {
61
+ const v0 = s.version;
62
+ const sameDelete = c.kind === "deleted" && c.version === v0;
63
+ const reconnected = c.kind === "active" && c.generation === v0 + 1;
64
+ if (userConnect && full) {
65
+ if (sameDelete)
66
+ return { kind: "reconnect", target: v0 + 1 };
67
+ if (reconnected)
68
+ return { kind: "publish", target: v0 + 1 };
69
+ }
70
+ return {
71
+ ...DISCARD,
72
+ restartFull: userConnect && !full && (sameDelete || reconnected),
73
+ };
74
+ }
75
+ case "foreign":
76
+ // No table row starts from foreign (DESIGN-QUESTIONS.md Q2).
77
+ return DISCARD;
78
+ }
79
+ }
80
+ export function decide(input) {
81
+ const decision = table(input);
82
+ if (input.phase === "after_write" && decision.kind !== "publish") {
83
+ // §2.4: after create/reconnect, C must re-read as active(T), else discard.
84
+ return DISCARD;
85
+ }
86
+ if (input.phase === "stale_base") {
87
+ // P0-RULINGS F1: a stream head of a newer generation than T exists, so
88
+ // the start snapshot is older than the registry. The table normally
89
+ // discards (with its §2.4 checkpoint rule). If control still admits T,
90
+ // the registry contradicts itself: keep pending, never publish or write.
91
+ if (decision.kind === "discard" || decision.kind === "keep_pending") {
92
+ return decision;
93
+ }
94
+ return { kind: "keep_pending", reason: "registry_inconsistent" };
95
+ }
96
+ return decision;
97
+ }
98
+ /** The target generation a start state admits, or `null` (unknown, foreign). */
99
+ export function targetFor(start) {
100
+ switch (start.kind) {
101
+ case "absent":
102
+ return 1;
103
+ case "active":
104
+ return start.generation;
105
+ case "deleted":
106
+ return start.version + 1;
107
+ default:
108
+ return null;
109
+ }
110
+ }
111
+ /**
112
+ * §2.4 checkpoint save: after every stream of the run is committed or no-op
113
+ * at T, re-read control. Save tagged (ns, T) only if it is `active(T)` and
114
+ * no stream was published `complete:false` or left a `complete:false` head
115
+ * unchanged (no-op, ruling Q9). A `record_too_large` exclusion
116
+ * does not block; a `bucket_too_large` stream is not committed and does. §2.9:
117
+ * the checkpoint also waits for every seed receipt of the run (a host merging its retained history).
118
+ */
119
+ export function checkpointDecision(input) {
120
+ const results = Object.values(input.streams);
121
+ for (const result of results) {
122
+ if (result.state !== "committed" && result.state !== "noop") {
123
+ return { kind: "skip", reason: "stream_not_committed" };
124
+ }
125
+ if (result.generation !== input.target) {
126
+ return { kind: "skip", reason: "stream_not_committed" };
127
+ }
128
+ }
129
+ for (const result of results) {
130
+ // Q9 ruling: a no-op onto a `complete:false` base blocks the checkpoint
131
+ // the same as publishing `complete:false` (liveness L3).
132
+ if ("complete" in result && !result.complete) {
133
+ return { kind: "skip", reason: "published_incomplete" };
134
+ }
135
+ }
136
+ if (input.seedReceiptsPending)
137
+ return { kind: "skip", reason: "seed_receipts_pending" };
138
+ if (input.control.kind !== "active" ||
139
+ input.control.generation !== input.target) {
140
+ return { kind: "skip", reason: "control_changed" };
141
+ }
142
+ return { kind: "save", generation: input.target };
143
+ }
144
+ //# sourceMappingURL=decide.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decide.js","sourceRoot":"","sources":["../src/decide.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * §2.10 the one eligibility predicate, used by reads, consent readiness and
3
+ * memory totals.
4
+ *
5
+ * For connector c and a requested name:
6
+ * 1. If every stream the name needs is live and `complete` in the selected
7
+ * connection → serve the stream view. A legacy file is never compared
8
+ * with a complete view (one-way).
9
+ * 2. Else, if any connection of c is deleted and not since reconnected →
10
+ * `unavailable` (no legacy or raw fallback).
11
+ * 3. Else, if a legacy or raw file of that name exists → serve it.
12
+ * 4. Else `unavailable`.
13
+ *
14
+ * Selected connection: live (control `active`, so never `foreign`) with the
15
+ * most recently registered heads. A physical name selects its own connection.
16
+ */
17
+ import type { ControlState } from "./control.js";
18
+ export interface StreamHeadView {
19
+ /** §2.4 liveness (`isHeadLive`). */
20
+ readonly live: boolean;
21
+ readonly complete: boolean;
22
+ }
23
+ export interface ConnectionView {
24
+ readonly ns: string;
25
+ /** This server's last synced control state for the connection. */
26
+ readonly control: ControlState;
27
+ /**
28
+ * Registration order of the connection's most recently registered live
29
+ * head (larger is more recent), or `null` when it has none.
30
+ */
31
+ readonly latestHeadRegistration: number | null;
32
+ readonly streams: Readonly<Record<string, StreamHeadView>>;
33
+ }
34
+ export type ReadTarget =
35
+ /** `pdpp.<c>.<s>` or a legacy binding: the streams it needs. */
36
+ {
37
+ readonly kind: "logical";
38
+ readonly streams: readonly string[];
39
+ }
40
+ /** `pdpp.<c>~<ns>.<s>`: its own connection. */
41
+ | {
42
+ readonly kind: "physical";
43
+ readonly ns: string;
44
+ readonly streams: readonly string[];
45
+ };
46
+ export interface EligibilityInput {
47
+ /** Every connection of the connector known to this server. */
48
+ readonly connections: readonly ConnectionView[];
49
+ readonly target: ReadTarget;
50
+ /** A legacy or raw file of the requested name exists. */
51
+ readonly legacyFile: boolean;
52
+ }
53
+ export type Eligibility = {
54
+ readonly kind: "view";
55
+ readonly ns: string;
56
+ } | {
57
+ readonly kind: "legacy";
58
+ } | {
59
+ readonly kind: "unavailable";
60
+ readonly reason: "deleted" | "no_data";
61
+ };
62
+ /** The connection a plain (logical) name of the connector reads. */
63
+ export declare function selectConnection(connections: readonly ConnectionView[]): ConnectionView | null;
64
+ export declare function eligible(input: EligibilityInput): Eligibility;
65
+ /** Readiness is exactly "a read would not be `unavailable`". */
66
+ export declare function isReady(input: EligibilityInput): boolean;
67
+ //# sourceMappingURL=eligible.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eligible.d.ts","sourceRoot":"","sources":["../src/eligible.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAIjD,MAAM,WAAW,cAAc;IAC7B,oCAAoC;IACpC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,kEAAkE;IAClE,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAC/B;;;OAGG;IACH,QAAQ,CAAC,sBAAsB,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/C,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC;CAC5D;AAED,MAAM,MAAM,UAAU;AACpB,gEAAgE;AAC9D;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE;AACnE,+CAA+C;GAC7C;IACE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC,CAAC;AAEN,MAAM,WAAW,gBAAgB;IAC/B,8DAA8D;IAC9D,QAAQ,CAAC,WAAW,EAAE,SAAS,cAAc,EAAE,CAAC;IAChD,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,yDAAyD;IACzD,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;CAC9B;AAED,MAAM,MAAM,WAAW,GACnB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAC9C;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,GAC3B;IAAE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,SAAS,CAAA;CAAE,CAAC;AAU7E,oEAAoE;AACpE,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,SAAS,cAAc,EAAE,GACrC,cAAc,GAAG,IAAI,CAGvB;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,gBAAgB,GAAG,WAAW,CA0B7D;AAED,gEAAgE;AAChE,wBAAgB,OAAO,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAExD"}