@cleocode/core 2026.10.12 → 2026.10.13
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.
- package/dist/build-provenance.json +2 -2
- package/dist/cloud/nexus-auth.d.ts.map +1 -1
- package/dist/cloud/nexus-auth.js +11 -2
- package/dist/cloud/nexus-auth.js.map +1 -1
- package/dist/cloud/nexus-cloud-status.d.ts.map +1 -1
- package/dist/cloud/nexus-cloud-status.js +18 -5
- package/dist/cloud/nexus-cloud-status.js.map +1 -1
- package/dist/cloud/nexus-credentials.d.ts +3 -2
- package/dist/cloud/nexus-credentials.d.ts.map +1 -1
- package/dist/cloud/nexus-credentials.js +58 -14
- package/dist/cloud/nexus-credentials.js.map +1 -1
- package/dist/cloud/nexus-logout.d.ts.map +1 -1
- package/dist/cloud/nexus-logout.js +12 -2
- package/dist/cloud/nexus-logout.js.map +1 -1
- package/dist/cloud/nexus-vault-state.d.ts.map +1 -1
- package/dist/cloud/nexus-vault-state.js +6 -26
- package/dist/cloud/nexus-vault-state.js.map +1 -1
- package/dist/config/registry.d.ts +21 -0
- package/dist/config/registry.d.ts.map +1 -1
- package/dist/config/registry.js +41 -0
- package/dist/config/registry.js.map +1 -1
- package/dist/dispatch/mvi-projection.d.ts.map +1 -1
- package/dist/dispatch/mvi-projection.js +2 -0
- package/dist/dispatch/mvi-projection.js.map +1 -1
- package/dist/docs/docs-ops.d.ts +15 -14
- package/dist/docs/docs-ops.d.ts.map +1 -1
- package/dist/docs/docs-ops.js +32 -96
- package/dist/docs/docs-ops.js.map +1 -1
- package/dist/docs/docs-read-model.d.ts +19 -10
- package/dist/docs/docs-read-model.d.ts.map +1 -1
- package/dist/docs/docs-read-model.js +31 -47
- package/dist/docs/docs-read-model.js.map +1 -1
- package/dist/docs/docs-update.d.ts +51 -1
- package/dist/docs/docs-update.d.ts.map +1 -1
- package/dist/docs/docs-update.js +235 -13
- package/dist/docs/docs-update.js.map +1 -1
- package/dist/docs/doctor.d.ts +26 -4
- package/dist/docs/doctor.d.ts.map +1 -1
- package/dist/docs/doctor.js +260 -102
- package/dist/docs/doctor.js.map +1 -1
- package/dist/docs/fetch-result.d.ts +30 -10
- package/dist/docs/fetch-result.d.ts.map +1 -1
- package/dist/docs/fetch-result.js +100 -15
- package/dist/docs/fetch-result.js.map +1 -1
- package/dist/docs/index.d.ts +1 -0
- package/dist/docs/index.d.ts.map +1 -1
- package/dist/docs/index.js +1 -0
- package/dist/docs/index.js.map +1 -1
- package/dist/docs/legacy-doc-migration.d.ts +182 -0
- package/dist/docs/legacy-doc-migration.d.ts.map +1 -0
- package/dist/docs/legacy-doc-migration.js +480 -0
- package/dist/docs/legacy-doc-migration.js.map +1 -0
- package/dist/docs/publications-store.d.ts +67 -0
- package/dist/docs/publications-store.d.ts.map +1 -0
- package/dist/docs/publications-store.js +193 -0
- package/dist/docs/publications-store.js.map +1 -0
- package/dist/docs/publish-pr.d.ts.map +1 -1
- package/dist/docs/publish-pr.js +31 -18
- package/dist/docs/publish-pr.js.map +1 -1
- package/dist/docs/section-leases.d.ts +233 -0
- package/dist/docs/section-leases.d.ts.map +1 -0
- package/dist/docs/section-leases.js +344 -0
- package/dist/docs/section-leases.js.map +1 -0
- package/dist/docs/section-merge.d.ts +159 -0
- package/dist/docs/section-merge.d.ts.map +1 -0
- package/dist/docs/section-merge.js +215 -0
- package/dist/docs/section-merge.js.map +1 -0
- package/dist/docs/section-write-path.d.ts +191 -0
- package/dist/docs/section-write-path.d.ts.map +1 -0
- package/dist/docs/section-write-path.js +423 -0
- package/dist/docs/section-write-path.js.map +1 -0
- package/dist/docs/stats.d.ts +56 -0
- package/dist/docs/stats.d.ts.map +1 -0
- package/dist/docs/stats.js +119 -0
- package/dist/docs/stats.js.map +1 -0
- package/dist/gc/cleanup.d.ts +1 -1
- package/dist/gc/cleanup.d.ts.map +1 -1
- package/dist/gc/cleanup.js +10 -1
- package/dist/gc/cleanup.js.map +1 -1
- package/dist/gc/janitor.js +1 -1
- package/dist/gc/janitor.js.map +1 -1
- package/dist/internal.d.ts +1 -1
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +1 -1
- package/dist/internal.js.map +1 -1
- package/dist/memory/pipeline-manifest-sqlite.d.ts +34 -0
- package/dist/memory/pipeline-manifest-sqlite.d.ts.map +1 -1
- package/dist/memory/pipeline-manifest-sqlite.js +114 -0
- package/dist/memory/pipeline-manifest-sqlite.js.map +1 -1
- package/dist/nexus/analyze-orchestrator.d.ts +5 -1
- package/dist/nexus/analyze-orchestrator.d.ts.map +1 -1
- package/dist/nexus/analyze-orchestrator.js +25 -13
- package/dist/nexus/analyze-orchestrator.js.map +1 -1
- package/dist/nexus/assessment-store.d.ts +8 -4
- package/dist/nexus/assessment-store.d.ts.map +1 -1
- package/dist/nexus/assessment-store.js +18 -27
- package/dist/nexus/assessment-store.js.map +1 -1
- package/dist/orchestration/index.d.ts.map +1 -1
- package/dist/orchestration/index.js +6 -0
- package/dist/orchestration/index.js.map +1 -1
- package/dist/orchestration/lean-change.d.ts +104 -5
- package/dist/orchestration/lean-change.d.ts.map +1 -1
- package/dist/orchestration/lean-change.js +162 -6
- package/dist/orchestration/lean-change.js.map +1 -1
- package/dist/orchestration/spawn-prompt.d.ts +6 -1
- package/dist/orchestration/spawn-prompt.d.ts.map +1 -1
- package/dist/orchestration/spawn-prompt.js +3 -1
- package/dist/orchestration/spawn-prompt.js.map +1 -1
- package/dist/orchestration/spawn.d.ts.map +1 -1
- package/dist/orchestration/spawn.js +6 -0
- package/dist/orchestration/spawn.js.map +1 -1
- package/dist/release/open.d.ts.map +1 -1
- package/dist/release/open.js +6 -2
- package/dist/release/open.js.map +1 -1
- package/dist/release/plan.d.ts.map +1 -1
- package/dist/release/plan.js +33 -2
- package/dist/release/plan.js.map +1 -1
- package/dist/resources/admission-ledger.d.ts +5 -0
- package/dist/resources/admission-ledger.d.ts.map +1 -1
- package/dist/resources/admission-ledger.js +1 -1
- package/dist/resources/admission-ledger.js.map +1 -1
- package/dist/resources/run-admission.d.ts.map +1 -1
- package/dist/resources/run-admission.js +4 -1
- package/dist/resources/run-admission.js.map +1 -1
- package/dist/resources/run-governed.d.ts +29 -4
- package/dist/resources/run-governed.d.ts.map +1 -1
- package/dist/resources/run-governed.js +113 -73
- package/dist/resources/run-governed.js.map +1 -1
- package/dist/sentient/worktree-dispatch.d.ts +12 -1
- package/dist/sentient/worktree-dispatch.d.ts.map +1 -1
- package/dist/sentient/worktree-dispatch.js +19 -0
- package/dist/sentient/worktree-dispatch.js.map +1 -1
- package/dist/spawn/branch-lock.d.ts +11 -1
- package/dist/spawn/branch-lock.d.ts.map +1 -1
- package/dist/spawn/branch-lock.js +54 -4
- package/dist/spawn/branch-lock.js.map +1 -1
- package/dist/sticky/archive.d.ts +1 -0
- package/dist/sticky/archive.d.ts.map +1 -1
- package/dist/sticky/archive.js +13 -0
- package/dist/sticky/archive.js.map +1 -1
- package/dist/sticky/engine-ops.d.ts.map +1 -1
- package/dist/sticky/engine-ops.js +7 -0
- package/dist/sticky/engine-ops.js.map +1 -1
- package/dist/store/attachment-repair.d.ts +10 -4
- package/dist/store/attachment-repair.d.ts.map +1 -1
- package/dist/store/attachment-repair.js +52 -25
- package/dist/store/attachment-repair.js.map +1 -1
- package/dist/store/attachment-store.d.ts.map +1 -1
- package/dist/store/attachment-store.js +27 -18
- package/dist/store/attachment-store.js.map +1 -1
- package/dist/store/blob-keep.d.ts +83 -0
- package/dist/store/blob-keep.d.ts.map +1 -1
- package/dist/store/blob-keep.js +139 -0
- package/dist/store/blob-keep.js.map +1 -1
- package/dist/store/blob-ops.d.ts +24 -0
- package/dist/store/blob-ops.d.ts.map +1 -1
- package/dist/store/blob-ops.js +47 -0
- package/dist/store/blob-ops.js.map +1 -1
- package/dist/store/conduit-doc-history.d.ts +209 -0
- package/dist/store/conduit-doc-history.d.ts.map +1 -1
- package/dist/store/conduit-doc-history.js +315 -3
- package/dist/store/conduit-doc-history.js.map +1 -1
- package/dist/store/decision-adr-links.d.ts +11 -0
- package/dist/store/decision-adr-links.d.ts.map +1 -1
- package/dist/store/decision-adr-links.js +20 -1
- package/dist/store/decision-adr-links.js.map +1 -1
- package/dist/store/display-id-alias.d.ts +8 -2
- package/dist/store/display-id-alias.d.ts.map +1 -1
- package/dist/store/display-id-alias.js +8 -3
- package/dist/store/display-id-alias.js.map +1 -1
- package/dist/store/lock.d.ts +18 -0
- package/dist/store/lock.d.ts.map +1 -1
- package/dist/store/lock.js +41 -0
- package/dist/store/lock.js.map +1 -1
- package/dist/store/row-identity-registry.d.ts.map +1 -1
- package/dist/store/row-identity-registry.js +17 -0
- package/dist/store/row-identity-registry.js.map +1 -1
- package/dist/store/schema/cleo-project/conduit.d.ts +16 -0
- package/dist/store/schema/cleo-project/conduit.d.ts.map +1 -1
- package/dist/store/schema/cleo-project/conduit.js +8 -0
- package/dist/store/schema/cleo-project/conduit.js.map +1 -1
- package/dist/store/schema/cleo-project/docs.d.ts +294 -0
- package/dist/store/schema/cleo-project/docs.d.ts.map +1 -1
- package/dist/store/schema/cleo-project/docs.js +82 -0
- package/dist/store/schema/cleo-project/docs.js.map +1 -1
- package/dist/store/sync/hlc.d.ts +9 -0
- package/dist/store/sync/hlc.d.ts.map +1 -1
- package/dist/store/sync/hlc.js +9 -0
- package/dist/store/sync/hlc.js.map +1 -1
- package/dist/store/sync/merge/engine.d.ts.map +1 -1
- package/dist/store/sync/merge/engine.js +4 -2
- package/dist/store/sync/merge/engine.js.map +1 -1
- package/dist/store/sync/merge/rules.d.ts +12 -1
- package/dist/store/sync/merge/rules.d.ts.map +1 -1
- package/dist/store/sync/merge/rules.js +76 -1
- package/dist/store/sync/merge/rules.js.map +1 -1
- package/dist/store/sync/replica-registry.d.ts +6 -2
- package/dist/store/sync/replica-registry.d.ts.map +1 -1
- package/dist/store/sync/replica-registry.js +21 -9
- package/dist/store/sync/replica-registry.js.map +1 -1
- package/dist/store/sync/timestamp-canon.d.ts.map +1 -1
- package/dist/store/sync/timestamp-canon.js +6 -0
- package/dist/store/sync/timestamp-canon.js.map +1 -1
- package/dist/store/table-classification.d.ts +58 -0
- package/dist/store/table-classification.d.ts.map +1 -1
- package/dist/store/table-classification.js +72 -0
- package/dist/store/table-classification.js.map +1 -1
- package/dist/store/twin-collapse.d.ts.map +1 -1
- package/dist/store/twin-collapse.js +4 -1
- package/dist/store/twin-collapse.js.map +1 -1
- package/dist/store/vault-manifest.d.ts +10 -1
- package/dist/store/vault-manifest.d.ts.map +1 -1
- package/dist/store/vault-manifest.js +59 -1
- package/dist/store/vault-manifest.js.map +1 -1
- package/dist/tasks/affected-scope.d.ts +11 -0
- package/dist/tasks/affected-scope.d.ts.map +1 -1
- package/dist/tasks/affected-scope.js +30 -11
- package/dist/tasks/affected-scope.js.map +1 -1
- package/dist/tasks/cancel-ops.d.ts.map +1 -1
- package/dist/tasks/cancel-ops.js +7 -2
- package/dist/tasks/cancel-ops.js.map +1 -1
- package/dist/tasks/change-set.d.ts +22 -0
- package/dist/tasks/change-set.d.ts.map +1 -1
- package/dist/tasks/change-set.js +119 -2
- package/dist/tasks/change-set.js.map +1 -1
- package/dist/tasks/complete.d.ts.map +1 -1
- package/dist/tasks/complete.js +7 -0
- package/dist/tasks/complete.js.map +1 -1
- package/dist/tasks/done-plan.d.ts +5 -0
- package/dist/tasks/done-plan.d.ts.map +1 -1
- package/dist/tasks/done-plan.js +54 -19
- package/dist/tasks/done-plan.js.map +1 -1
- package/dist/tasks/engine-wrap.d.ts +4 -0
- package/dist/tasks/engine-wrap.d.ts.map +1 -1
- package/dist/tasks/engine-wrap.js +20 -4
- package/dist/tasks/engine-wrap.js.map +1 -1
- package/dist/tasks/evidence-paths.d.ts +11 -0
- package/dist/tasks/evidence-paths.d.ts.map +1 -1
- package/dist/tasks/evidence-paths.js +12 -0
- package/dist/tasks/evidence-paths.js.map +1 -1
- package/dist/tools/fs.d.ts +7 -3
- package/dist/tools/fs.d.ts.map +1 -1
- package/dist/tools/fs.js +23 -8
- package/dist/tools/fs.js.map +1 -1
- package/dist/worktree/list.d.ts.map +1 -1
- package/dist/worktree/list.js +9 -0
- package/dist/worktree/list.js.map +1 -1
- package/dist/worktree/prune.d.ts.map +1 -1
- package/dist/worktree/prune.js +52 -1
- package/dist/worktree/prune.js.map +1 -1
- package/migrations/drizzle-cleo-project/20261013000000_t13360-migrated-doc-import/migration.sql +61 -0
- package/migrations/drizzle-cleo-project/20261014000000_t13362-docs-publications-ledger/migration.sql +45 -0
- package/package.json +13 -13
- package/templates/CLEO-INJECTION.md +1 -1
- package/templates/CLEO-REFERENCE.md +2 -2
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local section lease manager — T13356 (Epic T13340 / Saga T13339 · spec
|
|
3
|
+
* `cleodocs-rearch-design` §2 option b).
|
|
4
|
+
*
|
|
5
|
+
* ## Matched semantics
|
|
6
|
+
*
|
|
7
|
+
* llmtxt ships a `LeaseManager` concept (`dist/leases.js`, task T285) but
|
|
8
|
+
* the `llmtxt/leases` subpath is missing from the package exports map
|
|
9
|
+
* (upstream kryptobaseddev/llmtxt#16), and the class that IS reachable
|
|
10
|
+
* through the main entry is a REST client (`baseUrl` + `apiKey` + `fetch`
|
|
11
|
+
* against `/api/v1/documents/:slug/sections/:sid/lease`) — there is no
|
|
12
|
+
* local server to call. T13356 AC1 therefore accepts this LOCAL lease
|
|
13
|
+
* module with identical semantics, which mirrors the upstream contract:
|
|
14
|
+
*
|
|
15
|
+
* > "Leases are ADVISORY — the CRDT layer still accepts writes from
|
|
16
|
+
* > non-holders. 409 is a social signal, not a hard lock." — llmtxt
|
|
17
|
+
* > dist/leases.js (T285)
|
|
18
|
+
*
|
|
19
|
+
* - **Identity** — a lease is `{ leaseId, holder, expiresAt }` scoped to
|
|
20
|
+
* `(slug, sectionId)`, exactly llmtxt's `Lease` interface. This module
|
|
21
|
+
* adds `sessionId`/`acquiredAt`/`reason` (the session tie T13356
|
|
22
|
+
* requires; llmtxt's server held equivalent holder metadata).
|
|
23
|
+
* - **Acquire** — `acquire(slug, sectionId, ttlSeconds, reason?)` with the
|
|
24
|
+
* TTL bounded to 1–300 s, llmtxt's documented `durationSeconds` range. A
|
|
25
|
+
* held, unexpired lease yields the 409 equivalent: a `LeaseConflict`
|
|
26
|
+
* naming `holder`/`expiresAt` (advisory result, not an error). The
|
|
27
|
+
* throwing variant's message matches llmtxt byte-for-byte:
|
|
28
|
+
* `Section is already leased by <holder> until <expiresAt>`.
|
|
29
|
+
* - **Release / renew** — holder-session-only (a different session cannot
|
|
30
|
+
* release or renew another holder's lease); renewing with no held lease
|
|
31
|
+
* fails, mirroring llmtxt's "No active lease to renew".
|
|
32
|
+
* - **Auto-renew** — `startAutoRenew(thresholdSeconds = 10)` renews when
|
|
33
|
+
* the TTL drops below the threshold, llmtxt's default.
|
|
34
|
+
* - **Expiry / staleness** — a lease never outlives its session without
|
|
35
|
+
* renew: only the owning session can renew, so a dead session's lease
|
|
36
|
+
* expires at `expiresAt`; an EXPIRED lease is stale and any session may
|
|
37
|
+
* take it over (`tookOverFrom` names the stale holder).
|
|
38
|
+
*
|
|
39
|
+
* ## Storage — local-only JSON, deliberately NOT a cleo.db table
|
|
40
|
+
*
|
|
41
|
+
* Leases NEVER replicate (spec §2: "leases are local-only rows, always").
|
|
42
|
+
* This module keeps them in `.cleo/section-leases.json`, written
|
|
43
|
+
* atomically (tmp file + rename, the `writeBackupSidecar` pattern,
|
|
44
|
+
* T12535). A JSON file — not a table — is the honest choice: no migration,
|
|
45
|
+
* no gate 21/28/36/37 classification ceremony for ephemeral advisory state,
|
|
46
|
+
* and it structurally cannot be mistaken for a syncing table. The known
|
|
47
|
+
* trade-off, accepted deliberately: read-modify-write across PROCESSES is
|
|
48
|
+
* last-write-wins (no cross-process lock); the lease is advisory, the CRDT
|
|
49
|
+
* layer preserves content regardless, and CLEO's store discipline is a
|
|
50
|
+
* single active writer per store. Promote to a classified local-only table
|
|
51
|
+
* if multi-process contention ever matters (the classification pattern is
|
|
52
|
+
* `section_crdt_states`, landed with T13354).
|
|
53
|
+
*
|
|
54
|
+
* Not wired into the section edit flow yet — that lands with the T13356
|
|
55
|
+
* wiring slice after #2144 merges.
|
|
56
|
+
*
|
|
57
|
+
* @module
|
|
58
|
+
* @task T13356
|
|
59
|
+
* @epic T13340
|
|
60
|
+
* @saga T13339
|
|
61
|
+
*/
|
|
62
|
+
import { randomUUID } from 'node:crypto';
|
|
63
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
64
|
+
import { dirname, join } from 'node:path';
|
|
65
|
+
import { getCleoDirAbsolute } from '../paths.js';
|
|
66
|
+
/** Minimum lease TTL in seconds (llmtxt's `durationSeconds` lower bound). */
|
|
67
|
+
export const MIN_LEASE_TTL_SECONDS = 1;
|
|
68
|
+
/** Maximum lease TTL in seconds (llmtxt's `durationSeconds` upper bound). */
|
|
69
|
+
export const MAX_LEASE_TTL_SECONDS = 300;
|
|
70
|
+
/** Default auto-renew threshold in seconds (llmtxt's default). */
|
|
71
|
+
export const DEFAULT_RENEW_THRESHOLD_SECONDS = 10;
|
|
72
|
+
/** Thrown by {@link acquireOrThrow} — mirrors llmtxt's `LeaseConflictError`. */
|
|
73
|
+
export class SectionLeaseConflictError extends Error {
|
|
74
|
+
/** The current holder agent id. */
|
|
75
|
+
holder;
|
|
76
|
+
/** When the blocking lease expires (ISO-8601). */
|
|
77
|
+
expiresAt;
|
|
78
|
+
constructor(holder, expiresAt) {
|
|
79
|
+
super(`Section is already leased by ${holder} until ${expiresAt}`);
|
|
80
|
+
this.name = 'SectionLeaseConflictError';
|
|
81
|
+
this.holder = holder;
|
|
82
|
+
this.expiresAt = expiresAt;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** Composite lease key (`${slug}::${sectionId}`). */
|
|
86
|
+
function leaseKey(slug, sectionId) {
|
|
87
|
+
return `${slug}::${sectionId}`;
|
|
88
|
+
}
|
|
89
|
+
/** Resolve the leases file path for a project root. */
|
|
90
|
+
function leasesFilePath(projectRoot) {
|
|
91
|
+
return join(getCleoDirAbsolute(projectRoot), 'section-leases.json');
|
|
92
|
+
}
|
|
93
|
+
/** Clamp a requested TTL into llmtxt's 1–300 s range. */
|
|
94
|
+
function clampTtl(seconds) {
|
|
95
|
+
if (!Number.isFinite(seconds))
|
|
96
|
+
return MIN_LEASE_TTL_SECONDS;
|
|
97
|
+
return Math.min(MAX_LEASE_TTL_SECONDS, Math.max(MIN_LEASE_TTL_SECONDS, Math.floor(seconds)));
|
|
98
|
+
}
|
|
99
|
+
/** Read the leases file, tolerating a missing or malformed one. */
|
|
100
|
+
function readLeaseFile(projectRoot) {
|
|
101
|
+
const path = leasesFilePath(projectRoot);
|
|
102
|
+
if (!existsSync(path))
|
|
103
|
+
return { version: 1, leases: {} };
|
|
104
|
+
try {
|
|
105
|
+
const parsed = JSON.parse(readFileSync(path, 'utf-8'));
|
|
106
|
+
if (parsed &&
|
|
107
|
+
typeof parsed === 'object' &&
|
|
108
|
+
parsed.leases &&
|
|
109
|
+
typeof parsed.leases === 'object') {
|
|
110
|
+
return { version: 1, leases: parsed.leases };
|
|
111
|
+
}
|
|
112
|
+
return { version: 1, leases: {} };
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return { version: 1, leases: {} };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Atomically persist the leases file (tmp file + rename, the
|
|
120
|
+
* `writeBackupSidecar` pattern): a crash mid-write can at worst leave an
|
|
121
|
+
* orphaned tmp file, never a torn JSON document.
|
|
122
|
+
*/
|
|
123
|
+
function writeLeaseFile(projectRoot, file) {
|
|
124
|
+
const dest = leasesFilePath(projectRoot);
|
|
125
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
126
|
+
const tmp = `${dest}.${process.pid}.${Date.now()}.tmp`;
|
|
127
|
+
writeFileSync(tmp, JSON.stringify(file, null, 2));
|
|
128
|
+
renameSync(tmp, dest);
|
|
129
|
+
}
|
|
130
|
+
/** Whether a lease has expired at `nowMs` (epoch ms). */
|
|
131
|
+
function isExpired(lease, nowMs) {
|
|
132
|
+
const expiryMs = Date.parse(lease.expiresAt);
|
|
133
|
+
return Number.isNaN(expiryMs) || nowMs >= expiryMs;
|
|
134
|
+
}
|
|
135
|
+
/** Conflict view of a lease. */
|
|
136
|
+
function conflictOf(lease) {
|
|
137
|
+
return {
|
|
138
|
+
leaseId: lease.leaseId,
|
|
139
|
+
holder: lease.holder,
|
|
140
|
+
sessionId: lease.sessionId,
|
|
141
|
+
expiresAt: lease.expiresAt,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Local advisory lease manager for section edit scopes.
|
|
146
|
+
*
|
|
147
|
+
* One manager per (process, session). State is file-backed, so a second
|
|
148
|
+
* manager on the same project sees this manager's leases (and vice versa).
|
|
149
|
+
*/
|
|
150
|
+
export class SectionLeaseManager {
|
|
151
|
+
projectRoot;
|
|
152
|
+
sessionId;
|
|
153
|
+
holder;
|
|
154
|
+
now;
|
|
155
|
+
autoRenewTimer = null;
|
|
156
|
+
constructor(projectRoot, options) {
|
|
157
|
+
if (options.sessionId.trim().length === 0) {
|
|
158
|
+
// @sync-invariant none:local-only a lease must be tied to a session; the leases file is local-only advisory state, not a synced row
|
|
159
|
+
throw new Error('SectionLeaseManager requires a non-empty sessionId');
|
|
160
|
+
}
|
|
161
|
+
this.projectRoot = projectRoot;
|
|
162
|
+
this.sessionId = options.sessionId;
|
|
163
|
+
this.holder = options.holder ?? options.sessionId;
|
|
164
|
+
this.now = options.now ?? Date.now;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Acquire an advisory lease on `(slug, sectionId)`.
|
|
168
|
+
*
|
|
169
|
+
* A held, unexpired lease yields `{ ok: false, conflict }` — the 409
|
|
170
|
+
* equivalent, an advisory signal rather than an error (llmtxt: "409 is a
|
|
171
|
+
* social signal, not a hard lock"). An EXPIRED lease is stale: any
|
|
172
|
+
* session may take it over, and `tookOverFrom` names the stale holder.
|
|
173
|
+
*
|
|
174
|
+
* @param slug - Document slug.
|
|
175
|
+
* @param sectionId - Section id.
|
|
176
|
+
* @param ttlSeconds - Requested TTL, clamped to 1–300 s (llmtxt's range).
|
|
177
|
+
* @param reason - Optional human-readable reason.
|
|
178
|
+
*/
|
|
179
|
+
async acquire(slug, sectionId, ttlSeconds, reason) {
|
|
180
|
+
const file = readLeaseFile(this.projectRoot);
|
|
181
|
+
const key = leaseKey(slug, sectionId);
|
|
182
|
+
const existing = file.leases[key];
|
|
183
|
+
const nowMs = this.now();
|
|
184
|
+
let tookOverFrom;
|
|
185
|
+
if (existing !== undefined && !isExpired(existing, nowMs)) {
|
|
186
|
+
return { ok: false, conflict: conflictOf(existing) };
|
|
187
|
+
}
|
|
188
|
+
if (existing !== undefined) {
|
|
189
|
+
tookOverFrom = conflictOf(existing);
|
|
190
|
+
}
|
|
191
|
+
const ttl = clampTtl(ttlSeconds);
|
|
192
|
+
const lease = {
|
|
193
|
+
leaseId: randomUUID(),
|
|
194
|
+
holder: this.holder,
|
|
195
|
+
sessionId: this.sessionId,
|
|
196
|
+
slug,
|
|
197
|
+
sectionId,
|
|
198
|
+
...(reason !== undefined ? { reason } : {}),
|
|
199
|
+
acquiredAt: new Date(nowMs).toISOString(),
|
|
200
|
+
expiresAt: new Date(nowMs + ttl * 1000).toISOString(),
|
|
201
|
+
};
|
|
202
|
+
file.leases[key] = lease;
|
|
203
|
+
writeLeaseFile(this.projectRoot, file);
|
|
204
|
+
return tookOverFrom !== undefined ? { ok: true, lease, tookOverFrom } : { ok: true, lease };
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Throwing variant of {@link acquire}: raises {@link SectionLeaseConflictError}
|
|
208
|
+
* with llmtxt's exact message shape when the section is held.
|
|
209
|
+
*
|
|
210
|
+
* @returns The acquired lease.
|
|
211
|
+
*/
|
|
212
|
+
async acquireOrThrow(slug, sectionId, ttlSeconds, reason) {
|
|
213
|
+
const result = await this.acquire(slug, sectionId, ttlSeconds, reason);
|
|
214
|
+
if (!result.ok) {
|
|
215
|
+
// @sync-invariant none:local-only the conflict is the advisory 409 equivalent; the leases file is local-only state, not a synced row
|
|
216
|
+
throw new SectionLeaseConflictError(result.conflict.holder, result.conflict.expiresAt);
|
|
217
|
+
}
|
|
218
|
+
return result.lease;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Release a lease — only the owning session may release it.
|
|
222
|
+
*
|
|
223
|
+
* @returns Whether a lease was removed (false when unheld or held by
|
|
224
|
+
* another session).
|
|
225
|
+
*/
|
|
226
|
+
async release(slug, sectionId) {
|
|
227
|
+
const file = readLeaseFile(this.projectRoot);
|
|
228
|
+
const key = leaseKey(slug, sectionId);
|
|
229
|
+
const existing = file.leases[key];
|
|
230
|
+
if (existing === undefined || existing.sessionId !== this.sessionId)
|
|
231
|
+
return false;
|
|
232
|
+
delete file.leases[key];
|
|
233
|
+
writeLeaseFile(this.projectRoot, file);
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Renew a lease, extending its TTL — only the owning session may renew.
|
|
238
|
+
*
|
|
239
|
+
* @param slug - Document slug.
|
|
240
|
+
* @param sectionId - Section id.
|
|
241
|
+
* @param ttlSeconds - New TTL, clamped to 1–300 s.
|
|
242
|
+
*/
|
|
243
|
+
async renew(slug, sectionId, ttlSeconds) {
|
|
244
|
+
const file = readLeaseFile(this.projectRoot);
|
|
245
|
+
const key = leaseKey(slug, sectionId);
|
|
246
|
+
const existing = file.leases[key];
|
|
247
|
+
if (existing === undefined)
|
|
248
|
+
return { ok: false, notHeld: true };
|
|
249
|
+
const nowMs = this.now();
|
|
250
|
+
if (isExpired(existing, nowMs))
|
|
251
|
+
return { ok: false, notHeld: true };
|
|
252
|
+
if (existing.sessionId !== this.sessionId) {
|
|
253
|
+
return { ok: false, conflict: conflictOf(existing) };
|
|
254
|
+
}
|
|
255
|
+
const ttl = clampTtl(ttlSeconds);
|
|
256
|
+
const lease = {
|
|
257
|
+
...existing,
|
|
258
|
+
expiresAt: new Date(nowMs + ttl * 1000).toISOString(),
|
|
259
|
+
};
|
|
260
|
+
file.leases[key] = lease;
|
|
261
|
+
writeLeaseFile(this.projectRoot, file);
|
|
262
|
+
return { ok: true, lease };
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Current lease status for `(slug, sectionId)`: the live (unexpired)
|
|
266
|
+
* lease, or `null` when free or expired. Never throws.
|
|
267
|
+
*/
|
|
268
|
+
async status(slug, sectionId) {
|
|
269
|
+
const file = readLeaseFile(this.projectRoot);
|
|
270
|
+
const existing = file.leases[leaseKey(slug, sectionId)];
|
|
271
|
+
if (existing === undefined || isExpired(existing, this.now()))
|
|
272
|
+
return null;
|
|
273
|
+
return existing;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Start an auto-renew loop (llmtxt parity): renews when the remaining TTL
|
|
277
|
+
* drops below `thresholdSeconds` (default 10 s). In-process timer; a
|
|
278
|
+
* manager that stops renewing (crash, dead session) lets the lease expire
|
|
279
|
+
* at its TTL, after which any session may take over.
|
|
280
|
+
*
|
|
281
|
+
* @param slug - Document slug.
|
|
282
|
+
* @param sectionId - Section id.
|
|
283
|
+
* @param ttlSeconds - TTL applied at each renew, clamped to 1–300 s.
|
|
284
|
+
* @param thresholdSeconds - Renew when the remaining TTL drops below this.
|
|
285
|
+
*/
|
|
286
|
+
startAutoRenew(slug, sectionId, ttlSeconds, thresholdSeconds = DEFAULT_RENEW_THRESHOLD_SECONDS) {
|
|
287
|
+
this.stopAutoRenew();
|
|
288
|
+
const intervalMs = Math.max(250, Math.floor((thresholdSeconds * 1000) / 2));
|
|
289
|
+
this.autoRenewTimer = setInterval(() => {
|
|
290
|
+
void (async () => {
|
|
291
|
+
const current = await this.status(slug, sectionId);
|
|
292
|
+
if (current === null) {
|
|
293
|
+
this.stopAutoRenew();
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const remainingMs = Date.parse(current.expiresAt) - this.now();
|
|
297
|
+
if (remainingMs <= thresholdSeconds * 1000) {
|
|
298
|
+
await this.renew(slug, sectionId, ttlSeconds);
|
|
299
|
+
}
|
|
300
|
+
})();
|
|
301
|
+
}, intervalMs);
|
|
302
|
+
// Advisory coordination only — never keep the process alive on it.
|
|
303
|
+
if (typeof this.autoRenewTimer.unref === 'function')
|
|
304
|
+
this.autoRenewTimer.unref();
|
|
305
|
+
}
|
|
306
|
+
/** Stop the auto-renew loop (llmtxt parity). */
|
|
307
|
+
stopAutoRenew() {
|
|
308
|
+
if (this.autoRenewTimer !== null) {
|
|
309
|
+
clearInterval(this.autoRenewTimer);
|
|
310
|
+
this.autoRenewTimer = null;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/** Whether the auto-renew loop is running. */
|
|
314
|
+
get autoRenewRunning() {
|
|
315
|
+
return this.autoRenewTimer !== null;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Staleness predicate (T13356): a lease is stale — overwritable by any
|
|
320
|
+
* session — when its TTL has expired. (Session death alone does not kill an
|
|
321
|
+
* unexpired lease; conservatism is safe because a dead session cannot
|
|
322
|
+
* renew, so its lease is bounded by the TTL regardless.)
|
|
323
|
+
*
|
|
324
|
+
* @param lease - The lease to check.
|
|
325
|
+
* @param nowMs - Current time (epoch ms).
|
|
326
|
+
* @returns Whether the lease is stale.
|
|
327
|
+
*/
|
|
328
|
+
export function isStaleSectionLease(lease, nowMs) {
|
|
329
|
+
return isExpired(lease, nowMs);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* List the LIVE (unexpired) section leases of one doc. Expired leases are
|
|
333
|
+
* omitted: they are stale, and the next acquire takes them over.
|
|
334
|
+
*
|
|
335
|
+
* @param projectRoot - Absolute path to the CLEO project root.
|
|
336
|
+
* @param slug - The doc's live slug.
|
|
337
|
+
* @returns The live leases, in file order.
|
|
338
|
+
*/
|
|
339
|
+
export async function listSectionLeases(projectRoot, slug) {
|
|
340
|
+
const file = readLeaseFile(projectRoot);
|
|
341
|
+
const nowMs = Date.now();
|
|
342
|
+
return Object.values(file.leases).filter((lease) => lease.slug === slug && !isExpired(lease, nowMs));
|
|
343
|
+
}
|
|
344
|
+
//# sourceMappingURL=section-leases.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"section-leases.js","sourceRoot":"","sources":["../../src/docs/section-leases.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Section CRDT merge engine — the pure core of the CleoDocs re-architecture
|
|
3
|
+
* write path (T13355 · spec `cleodocs-rearch-design` §2 option b · D11165).
|
|
4
|
+
*
|
|
5
|
+
* Every document section is one Loro document (root `LoroText` named
|
|
6
|
+
* `content`), persisted as opaque snapshot bytes in the local-only
|
|
7
|
+
* `section_crdt_states` table. This module is the ONLY production importer of
|
|
8
|
+
* `llmtxt/crdt-primitives` (the llmtxt WASM-backed Loro primitives); the
|
|
9
|
+
* store orchestration in `section-write-path.ts` composes these functions
|
|
10
|
+
* with the canonical `conduit-doc-history.ts` accessors.
|
|
11
|
+
*
|
|
12
|
+
* Merge composition (spike finding, T13353): the WASM `crdt_merge_updates`
|
|
13
|
+
* is LOSSY in llmtxt@2026.5.15 — it returns an empty-doc snapshot for any
|
|
14
|
+
* input (kryptobaseddev/llmtxt#17, tracked in T13356). All merging here
|
|
15
|
+
* therefore composes updates via sequential {@link mergeSectionUpdates}
|
|
16
|
+
* (`crdt_apply_update`), which Loro makes idempotent and convergent:
|
|
17
|
+
* applying each peer's incremental update in turn yields every edit with an
|
|
18
|
+
* order-independent final text. `crdt_merge_updates` must NOT be used in the
|
|
19
|
+
* production path until the upstream fix lands.
|
|
20
|
+
*
|
|
21
|
+
* @module
|
|
22
|
+
* @task T13355
|
|
23
|
+
* @epic T13340
|
|
24
|
+
* @saga T13339
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* Deterministic section id for a split index. Section ids are positional
|
|
28
|
+
* (`s0` is the H1 + preamble, `s1` the first `## ` block, …): stable across
|
|
29
|
+
* devices for the same document shape and independent of heading text.
|
|
30
|
+
* Positional ids are a known simplification — heading-anchored ids are the
|
|
31
|
+
* filed follow-up (T13584).
|
|
32
|
+
*
|
|
33
|
+
* @param index - The section's position in {@link splitDocIntoSections}.
|
|
34
|
+
* @returns The section id recorded in `sections_modified`.
|
|
35
|
+
*/
|
|
36
|
+
export declare function sectionId(index: number): string;
|
|
37
|
+
/**
|
|
38
|
+
* Split a markdown document into sections deterministically: the H1 +
|
|
39
|
+
* preamble (everything before the first `## ` heading) is section 0; each
|
|
40
|
+
* `## ` heading starts a new section that runs (with its nested `###`
|
|
41
|
+
* blocks) to the next `## ` or EOF. Concatenating the sections with
|
|
42
|
+
* {@link joinSections} reproduces the input byte-for-byte, so the split is
|
|
43
|
+
* lossless and order-stable.
|
|
44
|
+
*
|
|
45
|
+
* @param markdown - The full document text.
|
|
46
|
+
* @returns The sections in document order.
|
|
47
|
+
*/
|
|
48
|
+
export declare function splitDocIntoSections(markdown: string): string[];
|
|
49
|
+
/**
|
|
50
|
+
* The inverse of {@link splitDocIntoSections}.
|
|
51
|
+
*
|
|
52
|
+
* @param sections - Sections in document order.
|
|
53
|
+
* @returns The reassembled document text.
|
|
54
|
+
*/
|
|
55
|
+
export declare function joinSections(sections: readonly string[]): string;
|
|
56
|
+
/**
|
|
57
|
+
* Seed a section Loro state from full text (a new or re-based section).
|
|
58
|
+
*
|
|
59
|
+
* @param text - The section's full text.
|
|
60
|
+
* @returns A Loro snapshot encoding the section.
|
|
61
|
+
*/
|
|
62
|
+
export declare function seedSectionState(text: string): Buffer;
|
|
63
|
+
/**
|
|
64
|
+
* Express one local edit as an incremental Loro update against a base
|
|
65
|
+
* state. The bundled primitives are append-shaped: the edit is the text to
|
|
66
|
+
* append to the base section (callers compute it as the tail of the new
|
|
67
|
+
* text when the old text is a prefix of it).
|
|
68
|
+
*
|
|
69
|
+
* @param base - The base section snapshot the edit was authored against.
|
|
70
|
+
* @param appended - The text the edit appends to the base.
|
|
71
|
+
* @returns The incremental update (the delta only, not a full snapshot).
|
|
72
|
+
*/
|
|
73
|
+
export declare function makeSectionEditUpdate(base: Buffer, appended: string): Buffer;
|
|
74
|
+
/**
|
|
75
|
+
* Merge incremental section updates onto a base snapshot by sequential
|
|
76
|
+
* `crdt_apply_update`. This is the production merge composition: Loro import
|
|
77
|
+
* is idempotent and convergent, so the result carries every applied edit and
|
|
78
|
+
* is independent of update order. Do NOT replace this with
|
|
79
|
+
* `crdt_merge_updates` — that WASM export is lossy in llmtxt@2026.5.15
|
|
80
|
+
* (T13353 spike, T13356, kryptobaseddev/llmtxt#17).
|
|
81
|
+
*
|
|
82
|
+
* @param base - The base section snapshot.
|
|
83
|
+
* @param updates - Peer incremental updates, in any order.
|
|
84
|
+
* @returns The merged section snapshot.
|
|
85
|
+
*/
|
|
86
|
+
export declare function mergeSectionUpdates(base: Buffer, updates: readonly Buffer[]): Buffer;
|
|
87
|
+
/**
|
|
88
|
+
* Extract the plain text of a section snapshot.
|
|
89
|
+
*
|
|
90
|
+
* @param state - A Loro snapshot from {@link seedSectionState} or {@link mergeSectionUpdates}.
|
|
91
|
+
* @returns The section's current text.
|
|
92
|
+
*/
|
|
93
|
+
export declare function sectionStateText(state: Buffer): string;
|
|
94
|
+
/**
|
|
95
|
+
* Apply one section's new text to its stored snapshot.
|
|
96
|
+
*
|
|
97
|
+
* `baseText` is the section text the incoming edit was AUTHORED AGAINST —
|
|
98
|
+
* the caller's concurrency reference (T13674). A base-less write passes the
|
|
99
|
+
* previous head's section text (single-writer assumption: the writer read
|
|
100
|
+
* the head and edited it). A write carrying a declared base passes the base
|
|
101
|
+
* version's section text, so a writer whose fetch predates a concurrent
|
|
102
|
+
* landing is recognized as stale instead of being mistaken for a
|
|
103
|
+
* single writer replacing the head.
|
|
104
|
+
*
|
|
105
|
+
* While the stored state equals the base text it holds no unlanded edit
|
|
106
|
+
* relative to what the writer saw, and any difference the writer authored
|
|
107
|
+
* is an ordinary edit — which the append-shaped primitives cannot express,
|
|
108
|
+
* so the section is re-seeded from the input and projects EXACTLY
|
|
109
|
+
* (mid-text replaces and deletions apply verbatim, never a fabricated
|
|
110
|
+
* concatenation — T13589). When the stored state differs from the base
|
|
111
|
+
* text, another writer's edit landed after the caller's fetch (T13674):
|
|
112
|
+
*
|
|
113
|
+
* - an incoming edit that is a pure APPEND relative to the common base
|
|
114
|
+
* (`nextText` extends `baseText`) composes onto the stored state, so
|
|
115
|
+
* both tails survive — this is never a fabrication;
|
|
116
|
+
* - any NON-append incoming edit (a replace or delete relative to the
|
|
117
|
+
* base) CANNOT be expressed with append-only primitives without
|
|
118
|
+
* inventing text, so the function reports `conflicted: true` and keeps
|
|
119
|
+
* the stored state untouched. The write path lands the incoming edit as
|
|
120
|
+
* its own append-only version row, keeps the stored projection as the
|
|
121
|
+
* head, and records the conflict on the head — both versions preserved,
|
|
122
|
+
* nothing fabricated.
|
|
123
|
+
*
|
|
124
|
+
* Semantics:
|
|
125
|
+
*
|
|
126
|
+
* - `stored === null` (first write of the section): seed a fresh state
|
|
127
|
+
* from the new text.
|
|
128
|
+
* - The stored text equals the new text: no-op (`changed: false`).
|
|
129
|
+
* - `baseText === null` (no base text available for this section — no
|
|
130
|
+
* head/base yet, or the reference version's blob is not resolvable
|
|
131
|
+
* locally): the single-writer assumption; re-seed from the new text
|
|
132
|
+
* (exact).
|
|
133
|
+
* - The stored text equals `baseText` (single writer against the
|
|
134
|
+
* reference): re-seed from the new text — the input projects exactly,
|
|
135
|
+
* deletions included.
|
|
136
|
+
* - The stored text differs from `baseText` and the incoming text equals
|
|
137
|
+
* `baseText`: the writer authored nothing new; no-op.
|
|
138
|
+
* - The stored text differs from `baseText` and the incoming text
|
|
139
|
+
* extends `baseText` (append): compose the incoming tail onto the
|
|
140
|
+
* stored state; both edits survive, `conflicted: false`.
|
|
141
|
+
* - The stored text differs from `baseText` and the incoming edit is a
|
|
142
|
+
* non-append relative to `baseText`: `conflicted: true`, stored state
|
|
143
|
+
* kept — the write path records the conflict and lands both versions.
|
|
144
|
+
*
|
|
145
|
+
* @param stored - The section's stored snapshot, or `null` when none exists.
|
|
146
|
+
* @param nextText - The section's new full text.
|
|
147
|
+
* @param baseText - The section's text in the version the incoming edit was
|
|
148
|
+
* authored against (previous head for base-less writes, declared base
|
|
149
|
+
* otherwise), or `null` when no reference text is available.
|
|
150
|
+
* @returns The resulting snapshot, whether it changed, and whether the edit
|
|
151
|
+
* is a non-append concurrent edit the caller must record as a conflict.
|
|
152
|
+
* @task T13674 (the baseText reference; T13589/T13595 for the merge shapes)
|
|
153
|
+
*/
|
|
154
|
+
export declare function applySectionTextEdit(stored: Buffer | null, nextText: string, baseText: string | null): {
|
|
155
|
+
snapshot: Buffer;
|
|
156
|
+
changed: boolean;
|
|
157
|
+
conflicted: boolean;
|
|
158
|
+
};
|
|
159
|
+
//# sourceMappingURL=section-merge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"section-merge.d.ts","sourceRoot":"","sources":["../../src/docs/section-merge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AASH;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAa/D;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAEhE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAErD;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAE5E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAMpF;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GAAG,IAAI,GACtB;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,CA8B7D"}
|