@owlmeans/planning-postgres 0.1.18-rc.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 (93) hide show
  1. package/README.md +88 -0
  2. package/agent-meta/manifest.json +16 -0
  3. package/agent-meta/skills/planning-postgres/SKILL.md +292 -0
  4. package/build/consts.d.ts +45 -0
  5. package/build/consts.d.ts.map +1 -0
  6. package/build/consts.js +45 -0
  7. package/build/consts.js.map +1 -0
  8. package/build/errors.d.ts +10 -0
  9. package/build/errors.d.ts.map +1 -0
  10. package/build/errors.js +13 -0
  11. package/build/errors.js.map +1 -0
  12. package/build/index.d.ts +9 -0
  13. package/build/index.d.ts.map +1 -0
  14. package/build/index.js +8 -0
  15. package/build/index.js.map +1 -0
  16. package/build/resource.d.ts +32 -0
  17. package/build/resource.d.ts.map +1 -0
  18. package/build/resource.js +94 -0
  19. package/build/resource.js.map +1 -0
  20. package/build/schemas.d.ts +20 -0
  21. package/build/schemas.d.ts.map +1 -0
  22. package/build/schemas.js +64 -0
  23. package/build/schemas.js.map +1 -0
  24. package/build/service.d.ts +16 -0
  25. package/build/service.d.ts.map +1 -0
  26. package/build/service.js +34 -0
  27. package/build/service.js.map +1 -0
  28. package/build/sql.d.ts +61 -0
  29. package/build/sql.d.ts.map +1 -0
  30. package/build/sql.js +90 -0
  31. package/build/sql.js.map +1 -0
  32. package/build/store/bus.d.ts +52 -0
  33. package/build/store/bus.d.ts.map +1 -0
  34. package/build/store/bus.js +167 -0
  35. package/build/store/bus.js.map +1 -0
  36. package/build/store/cards.d.ts +35 -0
  37. package/build/store/cards.d.ts.map +1 -0
  38. package/build/store/cards.js +112 -0
  39. package/build/store/cards.js.map +1 -0
  40. package/build/store/fold.d.ts +56 -0
  41. package/build/store/fold.d.ts.map +1 -0
  42. package/build/store/fold.js +453 -0
  43. package/build/store/fold.js.map +1 -0
  44. package/build/store/index.d.ts +22 -0
  45. package/build/store/index.d.ts.map +1 -0
  46. package/build/store/index.js +206 -0
  47. package/build/store/index.js.map +1 -0
  48. package/build/store/links.d.ts +18 -0
  49. package/build/store/links.d.ts.map +1 -0
  50. package/build/store/links.js +54 -0
  51. package/build/store/links.js.map +1 -0
  52. package/build/store/schemas.d.ts +32 -0
  53. package/build/store/schemas.d.ts.map +1 -0
  54. package/build/store/schemas.js +143 -0
  55. package/build/store/schemas.js.map +1 -0
  56. package/build/store/specs.d.ts +10 -0
  57. package/build/store/specs.d.ts.map +1 -0
  58. package/build/store/specs.js +47 -0
  59. package/build/store/specs.js.map +1 -0
  60. package/build/store/transitions.d.ts +44 -0
  61. package/build/store/transitions.d.ts.map +1 -0
  62. package/build/store/transitions.js +164 -0
  63. package/build/store/transitions.js.map +1 -0
  64. package/build/types.d.ts +72 -0
  65. package/build/types.d.ts.map +1 -0
  66. package/build/types.js +2 -0
  67. package/build/types.js.map +1 -0
  68. package/package.json +56 -0
  69. package/src/consts.ts +54 -0
  70. package/src/errors.ts +15 -0
  71. package/src/index.ts +9 -0
  72. package/src/resource.ts +133 -0
  73. package/src/schemas.ts +84 -0
  74. package/src/service.ts +45 -0
  75. package/src/sql.ts +135 -0
  76. package/src/store/bus.ts +217 -0
  77. package/src/store/cards.ts +153 -0
  78. package/src/store/fold.ts +560 -0
  79. package/src/store/index.ts +237 -0
  80. package/src/store/links.ts +77 -0
  81. package/src/store/schemas.ts +174 -0
  82. package/src/store/specs.ts +72 -0
  83. package/src/store/transitions.ts +221 -0
  84. package/src/types.ts +73 -0
  85. package/tests/bus.spec.ts +142 -0
  86. package/tests/conformance.spec.ts +37 -0
  87. package/tests/context.ts +143 -0
  88. package/tests/fold.spec.ts +213 -0
  89. package/tests/schema.spec.ts +89 -0
  90. package/tests/sql.spec.ts +50 -0
  91. package/tests/sync.spec.ts +61 -0
  92. package/tests/tsconfig.json +17 -0
  93. package/tsconfig.json +11 -0
package/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # @owlmeans/planning-postgres
2
+
3
+ A durable Postgres `PlanningStore` for `@owlmeans/server-planning`: projects, cards and
4
+ specifications in one table, the append-only transition log, typed links, and data-defined card
5
+ types and flows — four tables compiled from the planning record schemas by
6
+ `@owlmeans/postgres-resource`. A write folds inline, in one transaction per card under a Postgres
7
+ advisory lock; commits reach other processes over LISTEN/NOTIFY. Use it for a backend that keeps
8
+ its planning data in Postgres; a Mongo deployment uses its own store, and tests and single-process
9
+ tools the in-memory store of `@owlmeans/server-planning`.
10
+
11
+ ## Installation
12
+
13
+ ```sh
14
+ bun add @owlmeans/planning-postgres@^0.1.18-rc.0
15
+ ```
16
+
17
+ Peers: `pg`, `ajv`, `ajv-formats`.
18
+
19
+ ## Concepts
20
+
21
+ - **Four resources, four tables** — `planning-card` (projects, cards and specifications, routed by
22
+ `kind`), `planning-transition` (the log), `planning-link` (relationships), `planning-schema`
23
+ (data-defined types and flows). Timestamps stay `text`, so ISO-8601 order is row order.
24
+ - **Inline fold** — `project()` folds at once, inside one transaction under
25
+ `pg_advisory_xact_lock`; the settled events are delivered to waiters and `after` hooks only
26
+ after it commits.
27
+ - **A gap is not a failure** — `nextSeq` and `append` are two round trips, so a fold waits for a
28
+ young gap and fills an old one with a failed `lost-allocation` placeholder.
29
+ - **Every waiter heals** — a status read of a pending row older than `healAfterMs` folds its card
30
+ in the background; `recover()` folds what nobody is folding.
31
+ - **Data-defined types and flows** — the store implements the schema port, so the planning facade
32
+ exposes `definitions`.
33
+
34
+ ## Usage
35
+
36
+ ```ts
37
+ import { appendPostgres } from '@owlmeans/postgres'
38
+ import { appendPostgresPlanning } from '@owlmeans/planning-postgres'
39
+
40
+ appendPostgres(context)
41
+ appendPostgresPlanning(context, { plugins: [libraryPlugin] }) // four resources + the service
42
+
43
+ const planning = context.planning().for({ entityId, profileId })
44
+ const receipt = await planning.execute({
45
+ card: { kind: 'project', type: 'library:branch', title: 'Riverside branch' },
46
+ action: 'create',
47
+ }, { wait: true })
48
+ ```
49
+
50
+ A generated target registers each piece from its own file instead — `resources/planning/card.ts`
51
+ exporting `makeResource = makePlanningCardPostgres` (and `transition.ts`, `link.ts`, `schema.ts`),
52
+ and `services/planning.ts` exporting `makeService = () => makePostgresPlanningService({ plugins })`.
53
+
54
+ ## API
55
+
56
+ `makePlanningCardPostgres`, `makePlanningTransitionPostgres`, `makePlanningLinkPostgres`,
57
+ `makePlanningSchemaPostgres` (and `make*Resource(alias, dbAlias?, serviceAlias?)`,
58
+ `makePlanningPostgresResources`); `makePostgresPlanningStore`, `makePostgresPlanningService`,
59
+ `appendPostgresPlanning`; `PostgresPlanningStore` (`fold`, `recover`, `close`);
60
+ `DEFAULT_PLANNING_POSTGRES_LIMITS`, `RES_PLANNING_*`, `PLANNING_POSTGRES_STORE`;
61
+ `PlanningPostgresError`; the table schemas `Planning*TableSchema`.
62
+
63
+ ## Common pitfalls
64
+
65
+ - All four resources must be registered — the first call names the missing file.
66
+ - `close()` the store when a process ends on its own: the LISTEN connection keeps it alive.
67
+ - Do not aim `commits.subscribe` at hooks: `after` hooks run where the fold ran, once.
68
+
69
+ ## Related packages
70
+
71
+ `@owlmeans/planning`, `@owlmeans/server-planning`, `@owlmeans/client-planning`,
72
+ `@owlmeans/postgres-resource`, `@owlmeans/postgres`.
73
+
74
+ <!-- owlmeans:agent-guidance:start -->
75
+ ## Agent guidance
76
+
77
+ This package ships embedded agent skills under `agent-meta/`. After installing your
78
+ `@owlmeans/*` packages, run the OwlMeans agent-skills installer to place them into
79
+ your project's skill store (`.agents/skills/`):
80
+
81
+ ```sh
82
+ npx @owlmeans/agent-skills@^0.1.18-rc.41
83
+ ```
84
+
85
+ The embedded files are version-matched to this package release. Do not edit them
86
+ directly — they are regenerated on each publish. To contribute guidance edits,
87
+ open a PR against the source monorepo.
88
+ <!-- owlmeans:agent-guidance:end -->
@@ -0,0 +1,16 @@
1
+ {
2
+ "schemaVersion": 2,
3
+ "package": "@owlmeans/planning-postgres",
4
+ "version": "0.1.18-rc.0",
5
+ "generatedAt": "2026-09-28T02:06:37.869Z",
6
+ "canonicalRepo": "https://github.com/owlmeans/common",
7
+ "entries": [
8
+ {
9
+ "kind": "skill",
10
+ "name": "planning-postgres",
11
+ "category": "package-specific",
12
+ "file": "skills/planning-postgres/SKILL.md",
13
+ "canonicalPath": ".agents/skills/planning-postgres/SKILL.md"
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,292 @@
1
+ ---
2
+ name: planning-postgres
3
+ description: How to use @owlmeans/planning-postgres — the durable Postgres PlanningStore for @owlmeans/server-planning — the four resources (planning-card, planning-transition, planning-link, planning-schema) a target registers from resources/planning/*.ts, the service a target registers from services/planning.ts, the inline fold under a per-card advisory lock and its prelude, healing and recover(), the LISTEN/NOTIFY commit bus, the project purge, data-defined types and flows, the limits and the errors. Auto-invoked when wiring planning into a Postgres backend, touching a planning table, or diagnosing a planning commit that stays pending on Postgres.
4
+ user-invocable: false
5
+ ---
6
+ <!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
7
+
8
+ # @owlmeans/planning-postgres
9
+
10
+ **Layer:** Infra extension
11
+ **Install:** `"@owlmeans/planning-postgres": "^0.1.18-rc.0"` in `dependencies` (peers `pg`, `ajv`, `ajv-formats`)
12
+
13
+ A `PlanningStore` of `@owlmeans/server-planning` on Postgres. It owns no planning semantics: every
14
+ write still goes through the executor, every fold through `foldPending`, every query through
15
+ `criteriaOf` — this package supplies four tables, the transaction a fold runs in, and the bus that
16
+ carries commits between processes. It implements every port, the data-defined schema port
17
+ included, so a facade over it has `definitions`.
18
+
19
+ ## Key Exports
20
+
21
+ | Export | Description |
22
+ |--------|-------------|
23
+ | `makePlanningCardPostgres` / `makePlanningTransitionPostgres` / `makePlanningLinkPostgres` / `makePlanningSchemaPostgres` | `ResourceMaker`s of the four tables, under the default aliases |
24
+ | `makePlanningCardResource(alias?, dbAlias?, serviceAlias?)` (and the three twins), `makePlanningPostgresResources(aliases)` | The same resources under custom aliases |
25
+ | `makePostgresPlanningService(opts?, alias?)` | The planning host service over a Postgres store — a target's `makeService()` |
26
+ | `appendPostgresPlanning(ctx, opts?, alias?)` | The four resources (each unless present), the service and `ctx.planning()` in one call |
27
+ | `makePostgresPlanningStore({ context, ...opts })` | The store alone; `fold(card)`, `recover(opts?)`, `close()` beside the ports |
28
+ | `PlanningPostgresOptions` | `{ aliases?, bus?, limits?, ids?, now? }`; `PostgresPlanningServiceOptions` adds the service's own (`plugins`, `schemas`, `hooks`) |
29
+ | `DEFAULT_PLANNING_POSTGRES_LIMITS` | `{ foldBatch: 500, gapGraceMs: 30_000, healAfterMs: 1_000, recoverAfterMs: 60_000, lockTimeoutMs: 10_000 }` |
30
+ | `RES_PLANNING_CARD` · `RES_PLANNING_TRANSITION` · `RES_PLANNING_LINK` · `RES_PLANNING_SCHEMA`, `PLANNING_POSTGRES_STORE` | `planning-card` … `planning-schema`, `planning-postgres` |
31
+ | `Planning*TableSchema` | The table schemas, derived from the planning record schemas |
32
+ | `PlanningPostgresError` | `planning-postgres:<what>` — a fault (500) |
33
+ | `planningChannel(qualified)`, `LOST_ALLOCATION` | The bus channel of a transition table; the cause of a gap's placeholder |
34
+
35
+ ## The four tables
36
+
37
+ | Resource | Holds | Indexes |
38
+ |---|---|---|
39
+ | `planning-card` | projects, cards AND specifications (routed by `kind` at the query layer) | `(entityId, kind, type)`, `(parent, order)`, GIN `(parents)`, `(parent, status)`, `(entityId, intrinsic, updatedAt)`, GIN `(labels)`, `(entityId, parent, code) WHERE code IS NOT NULL`, `(parent, category) WHERE kind = 'specification'` |
40
+ | `planning-transition` | the append-only log | UNIQUE `(card, seq)`, UNIQUE `(entityId, key) WHERE key IS NOT NULL`, `(project, at)`, `(at) WHERE (commit->>'state') = 'pending'` |
41
+ | `planning-link` | typed edges | UNIQUE `(from, to, type)`, `(to, type)`, `(entityId, type)`, `(project)` |
42
+ | `planning-schema` | data-defined types and flows, and one private `kind: 'head'` row per organization (its schema revision) | UNIQUE `("entityId", COALESCE(project, ''), kind, key)`, `(entityId, rev)` |
43
+
44
+ - The DDL comes from `AnyWorkcardSchema`, `TransitionSchema`, `RelationshipSchema` and
45
+ `ScopedSchemaRecordSchema`. **Every timestamp stays `text`**: the `date-time` format would compile
46
+ to `timestamptz` and marshal through `Date`, which rewrites the ISO string a record carries and
47
+ breaks the lexicographic order `updatedSince`, `at` sorts and gap ages rely on. `seq`, `head`,
48
+ `revision` and `bodyChars` (and a schema record's `version` and `rev`) are `integer`; `body` is
49
+ `text`.
50
+ - The card table carries one private column, `headAt` — when `head` last moved — that no read
51
+ ever returns.
52
+ - A card list includes specifications only when it asks for `kind: 'specification'` or names a
53
+ `category` (`wantsSpecifications`, the memory store's own rule); a summary never counts them.
54
+ - Every maker declares each index once, however often it runs.
55
+
56
+ ## Target wiring
57
+
58
+ **Backs:** projects, cards and their documents as planning workcards — the transition log they are folded from, their links, and the card types and flows an application defines as data
59
+
60
+ | Sub-project | Packages |
61
+ |---|---|
62
+ | backend | `@owlmeans/planning-postgres`, `@owlmeans/server-planning`, `@owlmeans/planning` |
63
+ | common | `@owlmeans/planning` |
64
+ | api | `@owlmeans/server-planning`, `@owlmeans/planning` |
65
+
66
+ The service resolves its four resources by alias, so the registration order never matters. All
67
+ four are required: the store keeps data-defined types and flows too, and a write reads their
68
+ layer.
69
+
70
+ ```ts file=sources/backend/src/services/planning.ts
71
+ import { makePostgresPlanningService } from '@owlmeans/planning-postgres'
72
+ import type { Service } from '@owlmeans/context'
73
+ import { PLANNING } from '__APP_SLUG__-common/planning'
74
+
75
+ /**
76
+ * The planning service over Postgres — resolves its four resources
77
+ * (`resources/planning/{card,transition,link,schema}.ts`) by alias, so registration order never
78
+ * matters. The maker name is fixed — the generated service registry imports exactly this symbol.
79
+ */
80
+ export const makeService = (): Service => makePostgresPlanningService({ plugins: [PLANNING] })
81
+ ```
82
+
83
+ ```ts file=sources/backend/src/resources/planning/card.ts
84
+ import { makePlanningCardPostgres } from '@owlmeans/planning-postgres'
85
+ import type { PlanningCardRecord, PlanningCardResource } from '@owlmeans/planning-postgres'
86
+ import type { ResourceMaker } from '@owlmeans/resource'
87
+
88
+ /**
89
+ * Projects, cards and their documents — one table (`planning-card`), routed by `kind` at the
90
+ * query layer. The maker is a thin wrapper: the schema and indexes are the package's own.
91
+ */
92
+ export const makeResource: ResourceMaker<PlanningCardRecord, PlanningCardResource> =
93
+ (dbAlias, serviceAlias) => makePlanningCardPostgres(dbAlias, serviceAlias)
94
+ ```
95
+
96
+ ```ts file=sources/backend/src/resources/planning/transition.ts
97
+ import { makePlanningTransitionPostgres } from '@owlmeans/planning-postgres'
98
+ import type { PlanningTransitionResource } from '@owlmeans/planning-postgres'
99
+ import type { Transition } from '@owlmeans/planning'
100
+ import type { ResourceMaker } from '@owlmeans/resource'
101
+
102
+ /** The append-only transition log (`planning-transition`) every card is folded from. */
103
+ export const makeResource: ResourceMaker<Transition, PlanningTransitionResource> =
104
+ (dbAlias, serviceAlias) => makePlanningTransitionPostgres(dbAlias, serviceAlias)
105
+ ```
106
+
107
+ ```ts file=sources/backend/src/resources/planning/link.ts
108
+ import { makePlanningLinkPostgres } from '@owlmeans/planning-postgres'
109
+ import type { PlanningLinkResource } from '@owlmeans/planning-postgres'
110
+ import type { Relationship } from '@owlmeans/planning'
111
+ import type { ResourceMaker } from '@owlmeans/resource'
112
+
113
+ /** Typed links between cards (`planning-link`) — one row per from, to and type. */
114
+ export const makeResource: ResourceMaker<Relationship, PlanningLinkResource> =
115
+ (dbAlias, serviceAlias) => makePlanningLinkPostgres(dbAlias, serviceAlias)
116
+ ```
117
+
118
+ ```ts file=sources/backend/src/resources/planning/schema.ts
119
+ import { makePlanningSchemaPostgres } from '@owlmeans/planning-postgres'
120
+ import type { PlanningSchemaResource, PlanningSchemaRow } from '@owlmeans/planning-postgres'
121
+ import type { ResourceMaker } from '@owlmeans/resource'
122
+
123
+ /** Card types and flows defined as data (`planning-schema`), per organization and per project. */
124
+ export const makeResource: ResourceMaker<PlanningSchemaRow, PlanningSchemaResource> =
125
+ (dbAlias, serviceAlias) => makePlanningSchemaPostgres(dbAlias, serviceAlias)
126
+ ```
127
+
128
+ ```ts file=sources/common/src/planning.ts
129
+ import type { PlanningPlugin } from '@owlmeans/planning'
130
+
131
+ /**
132
+ * This application's planning types and flows — see the `planning` skill for their shape. The
133
+ * backend's planning service registers it; a client that reads a model loads the same bundle from
134
+ * the server, so nothing else imports it.
135
+ */
136
+ export const PLANNING: PlanningPlugin = {
137
+ name: 'app-planning',
138
+ schemas: {
139
+ flows: [
140
+ // owlmeans: add planning flows above this line
141
+ ],
142
+ types: [
143
+ // owlmeans: add planning types above this line
144
+ ],
145
+ },
146
+ }
147
+ ```
148
+
149
+ ## A worked example
150
+
151
+ The api binds the tree the shared package declares (an api's own `entrypoints.ts`, outside the
152
+ wiring above); a hand-written backend may register everything in one call instead —
153
+ `appendPostgres(context); appendPostgresPlanning(context, { plugins: [PLANNING] })`. A lending
154
+ library's branches (projects) and books (cards):
155
+
156
+ ```ts
157
+ // common: a lending library's branches (projects) and books (cards)
158
+ export const PLANNING: PlanningPlugin = {
159
+ name: 'app-planning',
160
+ schemas: {
161
+ flows: [{
162
+ id: 'library:circulation', version: 1,
163
+ statuses: [
164
+ { key: 'shelved', intrinsic: IntrinsicStatus.Planned, initial: true },
165
+ { key: 'lent', intrinsic: IntrinsicStatus.InProgress },
166
+ { key: 'retired', intrinsic: IntrinsicStatus.Closed },
167
+ ],
168
+ transitions: [
169
+ { name: 'lend', from: ['shelved'], to: 'lent', explicit: true },
170
+ { name: 'return', from: ['lent'], to: 'shelved', explicit: true },
171
+ ],
172
+ }],
173
+ types: [
174
+ { type: 'library:branch', kind: WorkcardKind.Project, version: 1, fields: { type: 'object' },
175
+ flows: ['library:circulation'], specifications: [], cardTypes: ['library:book'], scopedCardTypes: true },
176
+ { type: 'library:book', kind: WorkcardKind.Card, version: 1, fields: { type: 'object' },
177
+ flows: ['library:circulation'], specifications: [] },
178
+ ],
179
+ },
180
+ }
181
+ export const planningProtocols = makePlanningProtocols({
182
+ base: { alias: 'library:planning', path: '/planning' }, guards: DEFAULT_GUARD, definitions: true,
183
+ })
184
+
185
+ // api: the protocol bindings
186
+ ...servePlanningEntrypoints(planningProtocols)
187
+ ```
188
+
189
+ The first store call into a missing resource throws
190
+ `PlanningPostgresError('resource-missing:planning-schema: add src/resources/planning/schema.ts')`,
191
+ naming the file to add.
192
+
193
+ ## The fold
194
+
195
+ `project()` folds inline — there is no queued mode. One fold is ONE transaction:
196
+
197
+ 1. `SET LOCAL lock_timeout` (`limits.lockTimeoutMs`), then `pg_advisory_xact_lock` on
198
+ `advisoryKey('planning:<qualified card table>:<card id>')`.
199
+ 2. **The prelude** walks the rows past `card.seq` (at most `foldBatch`): a FAILED row at the cursor
200
+ moves the cursor past it; the PENDING rows from the cursor on are the run the fold takes; a GAP
201
+ — a row past the expected seq, or `head > seq` with no row at all — is an append still in
202
+ flight while younger than `gapGraceMs` (the fold stops; the appender's own `project()` folds
203
+ it), and a lost allocation after that: a failed `lost-allocation` placeholder per missing seq.
204
+ `nextSeq` and `append` are two round trips, so without it a transient gap or an already-failed
205
+ row would turn the next write into a spurious `fold:out-of-order`.
206
+ 3. `foldPending` of `@owlmeans/server-planning` over a view of the ports bound to the transaction,
207
+ limited to that run, each transition's writes in a SAVEPOINT (`FoldOptions.unit`) — so a write
208
+ the database refuses fails that transition alone and the fold goes past it. Prelude and fold
209
+ repeat within the transaction until the log is folded or a young gap stops it.
210
+ 4. A `pg_notify` per settled event (no record).
211
+ 5. COMMIT — and only then the events reach the commit hub and the `after` hooks, in order, outside
212
+ the transaction and the lock. A hook may therefore write to the card it saw commit.
213
+
214
+ A statement that fails outside a savepoint **poisons** the transaction: it is never committed
215
+ (the "transaction is aborted" answers after it are not the cause — the first failure is). The
216
+ fold is retried once; failing again, `failPending` fails the card's pending transitions in a fresh
217
+ transaction, so no waiter hangs to its timeout. A lock that cannot be taken within
218
+ `lockTimeoutMs` means another process is folding the card: nothing is failed, a heal follows.
219
+
220
+ Allocation: `nextSeq` is a compare-and-set of `head` on the card row, stamping `headAt`; a card
221
+ whose create has not folded counts `max(seq) + 1` from the log and the unique `(card, seq)` index
222
+ refuses a repeat. A card write never lowers `head` (`GREATEST`) and never touches `headAt`; a write
223
+ after the create is an UPDATE of a row that must still exist, so a card purged with its project is
224
+ not resurrected by a fold that was already under way.
225
+
226
+ ## Healing and recovery
227
+
228
+ - **Every waiter heals**: a status read of a pending row older than `healAfterMs` starts a
229
+ background fold of its card (try-lock, deduplicated per card).
230
+ - `recover({ olderThanMs = recoverAfterMs, limit = 100 })` folds each card whose oldest pending
231
+ row is older than the threshold (the partial pending index), and runs once in the background on
232
+ the store's first use.
233
+ - A lost allocation with no later row is released by the next fold of the card.
234
+
235
+ ## The commit bus
236
+
237
+ `makeCommitHub` is the commit source; LISTEN/NOTIFY feeds it across processes. The channel is
238
+ `planning_<16 hex>` of the qualified transition table, so two schemas in one database never hear
239
+ each other. The frame is `{ p: <process id>, t: 'c', e: <CommitEvent without record> }`; a process
240
+ ignores its own (`p`) — it delivered it itself, after its commit. LISTEN holds one dedicated
241
+ `pg.Client` built from the pool's own configuration (no pool slot), opened on the first subscribe,
242
+ wait or schema watch, reconnecting 1 s → 30 s; meanwhile the hub's poll ladder answers every
243
+ waiter. A schema write sends `{ t: 's', e: <entityId> }` on the same channel. `bus: false` sends
244
+ and hears nothing — waits poll, layers re-read the revision.
245
+
246
+ ## Data-defined types and flows
247
+
248
+ `planning-schema` holds one record per `(organization, project layer, kind, key)`; a write is one
249
+ transaction that bumps the organization's `head` row, writes under the compare-and-set on `version`
250
+ (an INSERT for version 1, an UPDATE guarded on `version - 1` after) and NOTIFYs. `revision()` is a
251
+ primary read of the head row, so the service's cached layers are never served stale. The rules —
252
+ what may be defined, sealing, retiring — are `planning`'s and `server-planning`'s.
253
+
254
+ ## Purge
255
+
256
+ A project's delete purges in the fold's own transaction (or, called directly, in one under the
257
+ project's lock): a recursive walk over `parents @> ARRAY[id]` finds every doomed card, nested
258
+ projects included; then links, transitions, the doomed projects' schema layers and the cards go,
259
+ in that order. The project's own `delete` rows stay as its tombstone — a waiter still reads the
260
+ delete committed.
261
+
262
+ ## Querying
263
+
264
+ Lists, counts and summaries go through `@owlmeans/postgres-resource` (`criteriaToSql`), so a
265
+ `WorkcardQuery` selects here what it selects in memory: `within` is `parents @> $1::varchar[]`,
266
+ `labels` `&&`, `flows.<id>` / `fields.<key>` typed jsonb paths (a list is membership), `q` `ILIKE` +
267
+ code prefix. A summary is one `countBy(parent, intrinsic)`. Paging is Postgres's — 100 rows unless
268
+ `size` says otherwise.
269
+
270
+ ## Gotchas
271
+
272
+ - `close()` the store in a process that should end on its own — the LISTEN connection keeps it
273
+ alive.
274
+ - Two fold transactions of one card never run at once in a process (chained) or across processes
275
+ (the advisory lock); a heal that finds the lock taken yields.
276
+ - A row appended by hand (`store.transitions.append`) is folded only by `project()`, a heal or
277
+ `recover()` — nothing watches the table.
278
+
279
+ ## Testing
280
+
281
+ `bun test ./tests` — `schema.spec.ts` and `sql.spec.ts` need no database; `conformance.spec.ts`
282
+ (the `@owlmeans/server-planning/conformance` cases), `fold.spec.ts`, `bus.spec.ts` and
283
+ `sync.spec.ts` are gated on `POSTGRES_URL` (`postgresGate()`) and skip cleanly without it. Each spec
284
+ file owns a throwaway schema (`makeSuite`); a fault is injected with a real trigger, never a mock.
285
+
286
+ ## Related
287
+
288
+ - `server-planning` — the executor, `foldPending`/`failPending`, the commit hub, the conformance suite
289
+ - `planning` — records, flows, scoped schemas, the query language
290
+ - `postgres-resource` — the table compiler, `criteriaToSql`, `countBy`, `advisoryKey`
291
+ - `postgres` — the connection service these resources resolve through
292
+ - `marketing-consent-postgres` — the same resource-per-file wiring for another feature
@@ -0,0 +1,45 @@
1
+ /** The four resource aliases — each is also the physical table name (sanitized). */
2
+ export declare const RES_PLANNING_CARD = "planning-card";
3
+ export declare const RES_PLANNING_TRANSITION = "planning-transition";
4
+ export declare const RES_PLANNING_LINK = "planning-link";
5
+ export declare const RES_PLANNING_SCHEMA = "planning-schema";
6
+ /** The alias the store answers `PlanningStore.alias` with, and the service its placeholders name. */
7
+ export declare const PLANNING_POSTGRES_STORE = "planning-postgres";
8
+ export declare const DEFAULT_PLANNING_POSTGRES_LIMITS: Readonly<{
9
+ /** The most log rows one fold transaction reads and applies; the rest follow in the next round. */
10
+ foldBatch: 500;
11
+ /**
12
+ * How long a gap in a card's log — a seq allocated with no row yet — is an append still in
13
+ * flight. Older, it is a lost allocation: the fold writes a failed placeholder and goes on.
14
+ */
15
+ gapGraceMs: 30000;
16
+ /** A pending row older than this makes every status read of it a heal: a background fold. */
17
+ healAfterMs: 1000;
18
+ /** What `recover()` folds by default: cards whose oldest pending row is older than this. */
19
+ recoverAfterMs: 60000;
20
+ /** `SET LOCAL lock_timeout` of a fold transaction, in milliseconds. */
21
+ lockTimeoutMs: 10000;
22
+ }>;
23
+ /** How many cards one `recover()` folds by default. */
24
+ export declare const DEFAULT_RECOVER_LIMIT = 100;
25
+ /** The cause and error of a failed placeholder written over a lost allocation. */
26
+ export declare const LOST_ALLOCATION = "lost-allocation";
27
+ /** Rounds (transactions) one `fold()` runs while a round reports more rows than `foldBatch`. */
28
+ export declare const MAX_FOLD_ROUNDS = 16;
29
+ /** Passes of prelude and fold one transaction runs — each over one run of consecutive pending rows. */
30
+ export declare const MAX_FOLD_PASSES = 64;
31
+ /** The private schema row that is an organization's monotonic schema revision. */
32
+ export declare const SCHEMA_HEAD_KIND = "head";
33
+ export declare const SCHEMA_HEAD_KEY = "head";
34
+ /** Reconnect backoff of the LISTEN connection, doubling from the first to the last, in milliseconds. */
35
+ export declare const BUS_BACKOFF: readonly [number, number];
36
+ /** Postgres refuses a NOTIFY payload of 8000 bytes or more; a frame stays under this. */
37
+ export declare const NOTIFY_PAYLOAD_MAX = 7900;
38
+ /** The file a target adds for each resource — what a missing-resource error names. */
39
+ export declare const PLANNING_RESOURCE_FILES: Readonly<{
40
+ card: "src/resources/planning/card.ts";
41
+ transition: "src/resources/planning/transition.ts";
42
+ link: "src/resources/planning/link.ts";
43
+ schema: "src/resources/planning/schema.ts";
44
+ }>;
45
+ //# sourceMappingURL=consts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAAA,oFAAoF;AACpF,eAAO,MAAM,iBAAiB,kBAAkB,CAAA;AAChD,eAAO,MAAM,uBAAuB,wBAAwB,CAAA;AAC5D,eAAO,MAAM,iBAAiB,kBAAkB,CAAA;AAChD,eAAO,MAAM,mBAAmB,oBAAoB,CAAA;AAEpD,qGAAqG;AACrG,eAAO,MAAM,uBAAuB,sBAAsB,CAAA;AAE1D,eAAO,MAAM,gCAAgC;IAC3C,mGAAmG;;IAEnG;;;OAGG;;IAEH,6FAA6F;;IAE7F,4FAA4F;;IAE5F,uEAAuE;;EAEvE,CAAA;AAEF,uDAAuD;AACvD,eAAO,MAAM,qBAAqB,MAAM,CAAA;AAExC,kFAAkF;AAClF,eAAO,MAAM,eAAe,oBAAoB,CAAA;AAEhD,gGAAgG;AAChG,eAAO,MAAM,eAAe,KAAK,CAAA;AAEjC,uGAAuG;AACvG,eAAO,MAAM,eAAe,KAAK,CAAA;AAEjC,kFAAkF;AAClF,eAAO,MAAM,gBAAgB,SAAS,CAAA;AACtC,eAAO,MAAM,eAAe,SAAS,CAAA;AAErC,wGAAwG;AACxG,eAAO,MAAM,WAAW,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,CAA+D,CAAA;AAEjH,yFAAyF;AACzF,eAAO,MAAM,kBAAkB,OAAQ,CAAA;AAEvC,sFAAsF;AACtF,eAAO,MAAM,uBAAuB;;;;;EAKlC,CAAA"}
@@ -0,0 +1,45 @@
1
+ /** The four resource aliases — each is also the physical table name (sanitized). */
2
+ export const RES_PLANNING_CARD = 'planning-card';
3
+ export const RES_PLANNING_TRANSITION = 'planning-transition';
4
+ export const RES_PLANNING_LINK = 'planning-link';
5
+ export const RES_PLANNING_SCHEMA = 'planning-schema';
6
+ /** The alias the store answers `PlanningStore.alias` with, and the service its placeholders name. */
7
+ export const PLANNING_POSTGRES_STORE = 'planning-postgres';
8
+ export const DEFAULT_PLANNING_POSTGRES_LIMITS = Object.freeze({
9
+ /** The most log rows one fold transaction reads and applies; the rest follow in the next round. */
10
+ foldBatch: 500,
11
+ /**
12
+ * How long a gap in a card's log — a seq allocated with no row yet — is an append still in
13
+ * flight. Older, it is a lost allocation: the fold writes a failed placeholder and goes on.
14
+ */
15
+ gapGraceMs: 30_000,
16
+ /** A pending row older than this makes every status read of it a heal: a background fold. */
17
+ healAfterMs: 1_000,
18
+ /** What `recover()` folds by default: cards whose oldest pending row is older than this. */
19
+ recoverAfterMs: 60_000,
20
+ /** `SET LOCAL lock_timeout` of a fold transaction, in milliseconds. */
21
+ lockTimeoutMs: 10_000,
22
+ });
23
+ /** How many cards one `recover()` folds by default. */
24
+ export const DEFAULT_RECOVER_LIMIT = 100;
25
+ /** The cause and error of a failed placeholder written over a lost allocation. */
26
+ export const LOST_ALLOCATION = 'lost-allocation';
27
+ /** Rounds (transactions) one `fold()` runs while a round reports more rows than `foldBatch`. */
28
+ export const MAX_FOLD_ROUNDS = 16;
29
+ /** Passes of prelude and fold one transaction runs — each over one run of consecutive pending rows. */
30
+ export const MAX_FOLD_PASSES = 64;
31
+ /** The private schema row that is an organization's monotonic schema revision. */
32
+ export const SCHEMA_HEAD_KIND = 'head';
33
+ export const SCHEMA_HEAD_KEY = 'head';
34
+ /** Reconnect backoff of the LISTEN connection, doubling from the first to the last, in milliseconds. */
35
+ export const BUS_BACKOFF = Object.freeze([1_000, 30_000]);
36
+ /** Postgres refuses a NOTIFY payload of 8000 bytes or more; a frame stays under this. */
37
+ export const NOTIFY_PAYLOAD_MAX = 7_900;
38
+ /** The file a target adds for each resource — what a missing-resource error names. */
39
+ export const PLANNING_RESOURCE_FILES = Object.freeze({
40
+ card: 'src/resources/planning/card.ts',
41
+ transition: 'src/resources/planning/transition.ts',
42
+ link: 'src/resources/planning/link.ts',
43
+ schema: 'src/resources/planning/schema.ts',
44
+ });
45
+ //# sourceMappingURL=consts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAAA,oFAAoF;AACpF,MAAM,CAAC,MAAM,iBAAiB,GAAG,eAAe,CAAA;AAChD,MAAM,CAAC,MAAM,uBAAuB,GAAG,qBAAqB,CAAA;AAC5D,MAAM,CAAC,MAAM,iBAAiB,GAAG,eAAe,CAAA;AAChD,MAAM,CAAC,MAAM,mBAAmB,GAAG,iBAAiB,CAAA;AAEpD,qGAAqG;AACrG,MAAM,CAAC,MAAM,uBAAuB,GAAG,mBAAmB,CAAA;AAE1D,MAAM,CAAC,MAAM,gCAAgC,GAAG,MAAM,CAAC,MAAM,CAAC;IAC5D,mGAAmG;IACnG,SAAS,EAAE,GAAG;IACd;;;OAGG;IACH,UAAU,EAAE,MAAM;IAClB,6FAA6F;IAC7F,WAAW,EAAE,KAAK;IAClB,4FAA4F;IAC5F,cAAc,EAAE,MAAM;IACtB,uEAAuE;IACvE,aAAa,EAAE,MAAM;CACtB,CAAC,CAAA;AAEF,uDAAuD;AACvD,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAG,CAAA;AAExC,kFAAkF;AAClF,MAAM,CAAC,MAAM,eAAe,GAAG,iBAAiB,CAAA;AAEhD,gGAAgG;AAChG,MAAM,CAAC,MAAM,eAAe,GAAG,EAAE,CAAA;AAEjC,uGAAuG;AACvG,MAAM,CAAC,MAAM,eAAe,GAAG,EAAE,CAAA;AAEjC,kFAAkF;AAClF,MAAM,CAAC,MAAM,gBAAgB,GAAG,MAAM,CAAA;AACtC,MAAM,CAAC,MAAM,eAAe,GAAG,MAAM,CAAA;AAErC,wGAAwG;AACxG,MAAM,CAAC,MAAM,WAAW,GAA8B,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,CAA8B,CAAA;AAEjH,yFAAyF;AACzF,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAK,CAAA;AAEvC,sFAAsF;AACtF,MAAM,CAAC,MAAM,uBAAuB,GAAG,MAAM,CAAC,MAAM,CAAC;IACnD,IAAI,EAAE,gCAAgC;IACtC,UAAU,EAAE,sCAAsC;IAClD,IAAI,EAAE,gCAAgC;IACtC,MAAM,EAAE,kCAAkC;CAC3C,CAAC,CAAA"}
@@ -0,0 +1,10 @@
1
+ import { ResilientError } from '@owlmeans/error';
2
+ /**
3
+ * A fault of the Postgres planning store — a missing resource, a fold transaction that could not
4
+ * commit. Messages start with `planning-postgres:`; it declares no HTTP status (a fault answers 500).
5
+ */
6
+ export declare class PlanningPostgresError extends ResilientError {
7
+ static typeName: string;
8
+ constructor(message?: string);
9
+ }
10
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD;;;GAGG;AACH,qBAAa,qBAAsB,SAAQ,cAAc;IACvD,OAAuB,QAAQ,EAAE,MAAM,CAA0B;IAEjE,YAAY,OAAO,GAAE,MAAgB,EAEpC;CACF"}
@@ -0,0 +1,13 @@
1
+ import { ResilientError } from '@owlmeans/error';
2
+ /**
3
+ * A fault of the Postgres planning store — a missing resource, a fold transaction that could not
4
+ * commit. Messages start with `planning-postgres:`; it declares no HTTP status (a fault answers 500).
5
+ */
6
+ export class PlanningPostgresError extends ResilientError {
7
+ static typeName = 'PlanningPostgresError';
8
+ constructor(message = 'error') {
9
+ super(PlanningPostgresError.typeName, `planning-postgres:${message}`);
10
+ }
11
+ }
12
+ ResilientError.registerErrorClass(PlanningPostgresError);
13
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD;;;GAGG;AACH,MAAM,OAAO,qBAAsB,SAAQ,cAAc;IAChD,MAAM,CAAU,QAAQ,GAAW,uBAAuB,CAAA;IAEjE,YAAY,OAAO,GAAW,OAAO;QACnC,KAAK,CAAC,qBAAqB,CAAC,QAAQ,EAAE,qBAAqB,OAAO,EAAE,CAAC,CAAA;IACvE,CAAC;CACF;AAED,cAAc,CAAC,kBAAkB,CAAC,qBAAqB,CAAC,CAAA"}
@@ -0,0 +1,9 @@
1
+ export type * from './types.js';
2
+ export * from './consts.js';
3
+ export * from './errors.js';
4
+ export * from './schemas.js';
5
+ export * from './resource.js';
6
+ export * from './sql.js';
7
+ export * from './store/index.js';
8
+ export * from './service.js';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,mBAAmB,YAAY,CAAA;AAE/B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,eAAe,CAAA;AAC7B,cAAc,UAAU,CAAA;AACxB,cAAc,kBAAkB,CAAA;AAChC,cAAc,cAAc,CAAA"}
package/build/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export * from './consts.js';
2
+ export * from './errors.js';
3
+ export * from './schemas.js';
4
+ export * from './resource.js';
5
+ export * from './sql.js';
6
+ export * from './store/index.js';
7
+ export * from './service.js';
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,eAAe,CAAA;AAC7B,cAAc,UAAU,CAAA;AACxB,cAAc,kBAAkB,CAAA;AAChC,cAAc,cAAc,CAAA"}
@@ -0,0 +1,32 @@
1
+ import type { Relationship, Transition } from '@owlmeans/planning';
2
+ import type { ResourceMaker } from '@owlmeans/resource';
3
+ import type { PlanningCardRecord, PlanningCardResource, PlanningLinkResource, PlanningPostgresAliases, PlanningSchemaResource, PlanningSchemaRow, PlanningTransitionResource } from './types.js';
4
+ /** The index names a table carries, by suffix — what the store matches a unique violation on. */
5
+ export declare const planningIndexName: (alias: string, suffix: string) => string;
6
+ /**
7
+ * `planning-card` — projects, cards and specifications in ONE table, routed by `kind` at the query
8
+ * layer. Indexes follow the reads: a list by kind and type, children in order, membership
9
+ * (`parents`, GIN), a board by status, the intrinsic state by recency, labels (GIN), a code within
10
+ * its parent, and a slot's documents.
11
+ */
12
+ export declare const makePlanningCardResource: (alias?: string, dbAlias?: string, serviceAlias?: string) => PlanningCardResource;
13
+ /**
14
+ * `planning-transition` — the append-only log. `(card, seq)` is the arbiter of allocation, a key is
15
+ * unique per organization, a project's history reads by time, and the pending rows by age (what
16
+ * `recover()` scans).
17
+ */
18
+ export declare const makePlanningTransitionResource: (alias?: string, dbAlias?: string, serviceAlias?: string) => PlanningTransitionResource;
19
+ /** `planning-link` — one row per `(from, to, type)`, read from either end, by type, and by project. */
20
+ export declare const makePlanningLinkResource: (alias?: string, dbAlias?: string, serviceAlias?: string) => PlanningLinkResource;
21
+ /**
22
+ * `planning-schema` — data-defined types and flows, one row per `(organization, project layer,
23
+ * kind, key)`, plus each organization's private revision row (`kind: 'head'`).
24
+ */
25
+ export declare const makePlanningSchemaResource: (alias?: string, dbAlias?: string, serviceAlias?: string) => PlanningSchemaResource;
26
+ export declare const makePlanningCardPostgres: ResourceMaker<PlanningCardRecord, PlanningCardResource>;
27
+ export declare const makePlanningTransitionPostgres: ResourceMaker<Transition, PlanningTransitionResource>;
28
+ export declare const makePlanningLinkPostgres: ResourceMaker<Relationship, PlanningLinkResource>;
29
+ export declare const makePlanningSchemaPostgres: ResourceMaker<PlanningSchemaRow, PlanningSchemaResource>;
30
+ /** The four resources under the aliases a store resolves — what `appendPostgresPlanning` registers. */
31
+ export declare const makePlanningPostgresResources: (aliases: PlanningPostgresAliases, dbAlias?: string, serviceAlias?: string) => [PlanningCardResource, PlanningTransitionResource, PlanningLinkResource, PlanningSchemaResource];
32
+ //# sourceMappingURL=resource.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAGlE,OAAO,KAAK,EAAE,aAAa,EAAkB,MAAM,oBAAoB,CAAA;AAKvE,OAAO,KAAK,EACV,kBAAkB,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,sBAAsB,EAC/G,iBAAiB,EAAE,0BAA0B,EAC9C,MAAM,YAAY,CAAA;AAoBnB,iGAAiG;AACjG,eAAO,MAAM,iBAAiB,UAAW,MAAM,UAAU,MAAM,KAAG,MAA4C,CAAA;AAE9G;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,WAC5B,MAAM,YAAgC,MAAM,iBAAiB,MAAM,KACzE,oBAeF,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,8BAA8B,WAClC,MAAM,YAAsC,MAAM,iBAAiB,MAAM,KAC/E,0BAWF,CAAA;AAED,uGAAuG;AACvG,eAAO,MAAM,wBAAwB,WAC5B,MAAM,YAAgC,MAAM,iBAAiB,MAAM,KACzE,oBAWF,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,0BAA0B,WAC9B,MAAM,YAAkC,MAAM,iBAAiB,MAAM,KAC3E,sBASF,CAAA;AAED,eAAO,MAAM,wBAAwB,EAAE,aAAa,CAAC,kBAAkB,EAAE,oBAAoB,CACE,CAAA;AAE/F,eAAO,MAAM,8BAA8B,EAAE,aAAa,CAAC,UAAU,EAAE,0BAA0B,CACU,CAAA;AAE3G,eAAO,MAAM,wBAAwB,EAAE,aAAa,CAAC,YAAY,EAAE,oBAAoB,CACQ,CAAA;AAE/F,eAAO,MAAM,0BAA0B,EAAE,aAAa,CAAC,iBAAiB,EAAE,sBAAsB,CACG,CAAA;AAEnG,uGAAuG;AACvG,eAAO,MAAM,6BAA6B,YAC/B,uBAAuB,YAAY,MAAM,iBAAiB,MAAM,KACxE,CAAC,oBAAoB,EAAE,0BAA0B,EAAE,oBAAoB,EAAE,sBAAsB,CAKjG,CAAA"}