@cleocode/core 2026.10.12 → 2026.10.13-rc.ci.682

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 (255) hide show
  1. package/dist/build-provenance.json +2 -2
  2. package/dist/cloud/nexus-auth.d.ts.map +1 -1
  3. package/dist/cloud/nexus-auth.js +11 -2
  4. package/dist/cloud/nexus-auth.js.map +1 -1
  5. package/dist/cloud/nexus-cloud-status.d.ts.map +1 -1
  6. package/dist/cloud/nexus-cloud-status.js +18 -5
  7. package/dist/cloud/nexus-cloud-status.js.map +1 -1
  8. package/dist/cloud/nexus-credentials.d.ts +3 -2
  9. package/dist/cloud/nexus-credentials.d.ts.map +1 -1
  10. package/dist/cloud/nexus-credentials.js +58 -14
  11. package/dist/cloud/nexus-credentials.js.map +1 -1
  12. package/dist/cloud/nexus-logout.d.ts.map +1 -1
  13. package/dist/cloud/nexus-logout.js +12 -2
  14. package/dist/cloud/nexus-logout.js.map +1 -1
  15. package/dist/cloud/nexus-vault-state.d.ts.map +1 -1
  16. package/dist/cloud/nexus-vault-state.js +6 -26
  17. package/dist/cloud/nexus-vault-state.js.map +1 -1
  18. package/dist/config/registry.d.ts +21 -0
  19. package/dist/config/registry.d.ts.map +1 -1
  20. package/dist/config/registry.js +41 -0
  21. package/dist/config/registry.js.map +1 -1
  22. package/dist/dispatch/mvi-projection.d.ts.map +1 -1
  23. package/dist/dispatch/mvi-projection.js +2 -0
  24. package/dist/dispatch/mvi-projection.js.map +1 -1
  25. package/dist/docs/docs-ops.d.ts +15 -14
  26. package/dist/docs/docs-ops.d.ts.map +1 -1
  27. package/dist/docs/docs-ops.js +32 -96
  28. package/dist/docs/docs-ops.js.map +1 -1
  29. package/dist/docs/docs-read-model.d.ts +19 -10
  30. package/dist/docs/docs-read-model.d.ts.map +1 -1
  31. package/dist/docs/docs-read-model.js +31 -47
  32. package/dist/docs/docs-read-model.js.map +1 -1
  33. package/dist/docs/docs-update.d.ts +51 -1
  34. package/dist/docs/docs-update.d.ts.map +1 -1
  35. package/dist/docs/docs-update.js +235 -13
  36. package/dist/docs/docs-update.js.map +1 -1
  37. package/dist/docs/doctor.d.ts +26 -4
  38. package/dist/docs/doctor.d.ts.map +1 -1
  39. package/dist/docs/doctor.js +260 -102
  40. package/dist/docs/doctor.js.map +1 -1
  41. package/dist/docs/fetch-result.d.ts +30 -10
  42. package/dist/docs/fetch-result.d.ts.map +1 -1
  43. package/dist/docs/fetch-result.js +100 -15
  44. package/dist/docs/fetch-result.js.map +1 -1
  45. package/dist/docs/index.d.ts +1 -0
  46. package/dist/docs/index.d.ts.map +1 -1
  47. package/dist/docs/index.js +1 -0
  48. package/dist/docs/index.js.map +1 -1
  49. package/dist/docs/legacy-doc-migration.d.ts +182 -0
  50. package/dist/docs/legacy-doc-migration.d.ts.map +1 -0
  51. package/dist/docs/legacy-doc-migration.js +480 -0
  52. package/dist/docs/legacy-doc-migration.js.map +1 -0
  53. package/dist/docs/publications-store.d.ts +67 -0
  54. package/dist/docs/publications-store.d.ts.map +1 -0
  55. package/dist/docs/publications-store.js +193 -0
  56. package/dist/docs/publications-store.js.map +1 -0
  57. package/dist/docs/publish-pr.d.ts.map +1 -1
  58. package/dist/docs/publish-pr.js +31 -18
  59. package/dist/docs/publish-pr.js.map +1 -1
  60. package/dist/docs/section-leases.d.ts +233 -0
  61. package/dist/docs/section-leases.d.ts.map +1 -0
  62. package/dist/docs/section-leases.js +344 -0
  63. package/dist/docs/section-leases.js.map +1 -0
  64. package/dist/docs/section-merge.d.ts +159 -0
  65. package/dist/docs/section-merge.d.ts.map +1 -0
  66. package/dist/docs/section-merge.js +215 -0
  67. package/dist/docs/section-merge.js.map +1 -0
  68. package/dist/docs/section-write-path.d.ts +191 -0
  69. package/dist/docs/section-write-path.d.ts.map +1 -0
  70. package/dist/docs/section-write-path.js +423 -0
  71. package/dist/docs/section-write-path.js.map +1 -0
  72. package/dist/docs/stats.d.ts +56 -0
  73. package/dist/docs/stats.d.ts.map +1 -0
  74. package/dist/docs/stats.js +119 -0
  75. package/dist/docs/stats.js.map +1 -0
  76. package/dist/gc/cleanup.d.ts +1 -1
  77. package/dist/gc/cleanup.d.ts.map +1 -1
  78. package/dist/gc/cleanup.js +10 -1
  79. package/dist/gc/cleanup.js.map +1 -1
  80. package/dist/gc/janitor.js +1 -1
  81. package/dist/gc/janitor.js.map +1 -1
  82. package/dist/internal.d.ts +1 -1
  83. package/dist/internal.d.ts.map +1 -1
  84. package/dist/internal.js +1 -1
  85. package/dist/internal.js.map +1 -1
  86. package/dist/memory/pipeline-manifest-sqlite.d.ts +34 -0
  87. package/dist/memory/pipeline-manifest-sqlite.d.ts.map +1 -1
  88. package/dist/memory/pipeline-manifest-sqlite.js +114 -0
  89. package/dist/memory/pipeline-manifest-sqlite.js.map +1 -1
  90. package/dist/nexus/analyze-orchestrator.d.ts +5 -1
  91. package/dist/nexus/analyze-orchestrator.d.ts.map +1 -1
  92. package/dist/nexus/analyze-orchestrator.js +25 -13
  93. package/dist/nexus/analyze-orchestrator.js.map +1 -1
  94. package/dist/nexus/assessment-store.d.ts +8 -4
  95. package/dist/nexus/assessment-store.d.ts.map +1 -1
  96. package/dist/nexus/assessment-store.js +18 -27
  97. package/dist/nexus/assessment-store.js.map +1 -1
  98. package/dist/orchestration/index.d.ts.map +1 -1
  99. package/dist/orchestration/index.js +6 -0
  100. package/dist/orchestration/index.js.map +1 -1
  101. package/dist/orchestration/lean-change.d.ts +104 -5
  102. package/dist/orchestration/lean-change.d.ts.map +1 -1
  103. package/dist/orchestration/lean-change.js +162 -6
  104. package/dist/orchestration/lean-change.js.map +1 -1
  105. package/dist/orchestration/spawn-prompt.d.ts +6 -1
  106. package/dist/orchestration/spawn-prompt.d.ts.map +1 -1
  107. package/dist/orchestration/spawn-prompt.js +3 -1
  108. package/dist/orchestration/spawn-prompt.js.map +1 -1
  109. package/dist/orchestration/spawn.d.ts.map +1 -1
  110. package/dist/orchestration/spawn.js +6 -0
  111. package/dist/orchestration/spawn.js.map +1 -1
  112. package/dist/release/open.d.ts.map +1 -1
  113. package/dist/release/open.js +6 -2
  114. package/dist/release/open.js.map +1 -1
  115. package/dist/release/plan.d.ts.map +1 -1
  116. package/dist/release/plan.js +33 -2
  117. package/dist/release/plan.js.map +1 -1
  118. package/dist/resources/admission-ledger.d.ts +5 -0
  119. package/dist/resources/admission-ledger.d.ts.map +1 -1
  120. package/dist/resources/admission-ledger.js +1 -1
  121. package/dist/resources/admission-ledger.js.map +1 -1
  122. package/dist/resources/run-admission.d.ts.map +1 -1
  123. package/dist/resources/run-admission.js +4 -1
  124. package/dist/resources/run-admission.js.map +1 -1
  125. package/dist/resources/run-governed.d.ts +29 -4
  126. package/dist/resources/run-governed.d.ts.map +1 -1
  127. package/dist/resources/run-governed.js +113 -73
  128. package/dist/resources/run-governed.js.map +1 -1
  129. package/dist/sentient/worktree-dispatch.d.ts +12 -1
  130. package/dist/sentient/worktree-dispatch.d.ts.map +1 -1
  131. package/dist/sentient/worktree-dispatch.js +19 -0
  132. package/dist/sentient/worktree-dispatch.js.map +1 -1
  133. package/dist/spawn/branch-lock.d.ts +11 -1
  134. package/dist/spawn/branch-lock.d.ts.map +1 -1
  135. package/dist/spawn/branch-lock.js +54 -4
  136. package/dist/spawn/branch-lock.js.map +1 -1
  137. package/dist/sticky/archive.d.ts +1 -0
  138. package/dist/sticky/archive.d.ts.map +1 -1
  139. package/dist/sticky/archive.js +13 -0
  140. package/dist/sticky/archive.js.map +1 -1
  141. package/dist/sticky/engine-ops.d.ts.map +1 -1
  142. package/dist/sticky/engine-ops.js +7 -0
  143. package/dist/sticky/engine-ops.js.map +1 -1
  144. package/dist/store/attachment-repair.d.ts +10 -4
  145. package/dist/store/attachment-repair.d.ts.map +1 -1
  146. package/dist/store/attachment-repair.js +52 -25
  147. package/dist/store/attachment-repair.js.map +1 -1
  148. package/dist/store/attachment-store.d.ts.map +1 -1
  149. package/dist/store/attachment-store.js +27 -18
  150. package/dist/store/attachment-store.js.map +1 -1
  151. package/dist/store/blob-keep.d.ts +83 -0
  152. package/dist/store/blob-keep.d.ts.map +1 -1
  153. package/dist/store/blob-keep.js +139 -0
  154. package/dist/store/blob-keep.js.map +1 -1
  155. package/dist/store/blob-ops.d.ts +24 -0
  156. package/dist/store/blob-ops.d.ts.map +1 -1
  157. package/dist/store/blob-ops.js +47 -0
  158. package/dist/store/blob-ops.js.map +1 -1
  159. package/dist/store/conduit-doc-history.d.ts +209 -0
  160. package/dist/store/conduit-doc-history.d.ts.map +1 -1
  161. package/dist/store/conduit-doc-history.js +315 -3
  162. package/dist/store/conduit-doc-history.js.map +1 -1
  163. package/dist/store/decision-adr-links.d.ts +11 -0
  164. package/dist/store/decision-adr-links.d.ts.map +1 -1
  165. package/dist/store/decision-adr-links.js +20 -1
  166. package/dist/store/decision-adr-links.js.map +1 -1
  167. package/dist/store/display-id-alias.d.ts +8 -2
  168. package/dist/store/display-id-alias.d.ts.map +1 -1
  169. package/dist/store/display-id-alias.js +8 -3
  170. package/dist/store/display-id-alias.js.map +1 -1
  171. package/dist/store/lock.d.ts +18 -0
  172. package/dist/store/lock.d.ts.map +1 -1
  173. package/dist/store/lock.js +41 -0
  174. package/dist/store/lock.js.map +1 -1
  175. package/dist/store/row-identity-registry.d.ts.map +1 -1
  176. package/dist/store/row-identity-registry.js +17 -0
  177. package/dist/store/row-identity-registry.js.map +1 -1
  178. package/dist/store/schema/cleo-project/conduit.d.ts +16 -0
  179. package/dist/store/schema/cleo-project/conduit.d.ts.map +1 -1
  180. package/dist/store/schema/cleo-project/conduit.js +8 -0
  181. package/dist/store/schema/cleo-project/conduit.js.map +1 -1
  182. package/dist/store/schema/cleo-project/docs.d.ts +294 -0
  183. package/dist/store/schema/cleo-project/docs.d.ts.map +1 -1
  184. package/dist/store/schema/cleo-project/docs.js +82 -0
  185. package/dist/store/schema/cleo-project/docs.js.map +1 -1
  186. package/dist/store/sync/hlc.d.ts +9 -0
  187. package/dist/store/sync/hlc.d.ts.map +1 -1
  188. package/dist/store/sync/hlc.js +9 -0
  189. package/dist/store/sync/hlc.js.map +1 -1
  190. package/dist/store/sync/merge/engine.d.ts.map +1 -1
  191. package/dist/store/sync/merge/engine.js +4 -2
  192. package/dist/store/sync/merge/engine.js.map +1 -1
  193. package/dist/store/sync/merge/rules.d.ts +12 -1
  194. package/dist/store/sync/merge/rules.d.ts.map +1 -1
  195. package/dist/store/sync/merge/rules.js +76 -1
  196. package/dist/store/sync/merge/rules.js.map +1 -1
  197. package/dist/store/sync/replica-registry.d.ts +6 -2
  198. package/dist/store/sync/replica-registry.d.ts.map +1 -1
  199. package/dist/store/sync/replica-registry.js +21 -9
  200. package/dist/store/sync/replica-registry.js.map +1 -1
  201. package/dist/store/sync/timestamp-canon.d.ts.map +1 -1
  202. package/dist/store/sync/timestamp-canon.js +6 -0
  203. package/dist/store/sync/timestamp-canon.js.map +1 -1
  204. package/dist/store/table-classification.d.ts +58 -0
  205. package/dist/store/table-classification.d.ts.map +1 -1
  206. package/dist/store/table-classification.js +72 -0
  207. package/dist/store/table-classification.js.map +1 -1
  208. package/dist/store/twin-collapse.d.ts.map +1 -1
  209. package/dist/store/twin-collapse.js +4 -1
  210. package/dist/store/twin-collapse.js.map +1 -1
  211. package/dist/store/vault-manifest.d.ts +10 -1
  212. package/dist/store/vault-manifest.d.ts.map +1 -1
  213. package/dist/store/vault-manifest.js +59 -1
  214. package/dist/store/vault-manifest.js.map +1 -1
  215. package/dist/tasks/affected-scope.d.ts +11 -0
  216. package/dist/tasks/affected-scope.d.ts.map +1 -1
  217. package/dist/tasks/affected-scope.js +30 -11
  218. package/dist/tasks/affected-scope.js.map +1 -1
  219. package/dist/tasks/cancel-ops.d.ts.map +1 -1
  220. package/dist/tasks/cancel-ops.js +7 -2
  221. package/dist/tasks/cancel-ops.js.map +1 -1
  222. package/dist/tasks/change-set.d.ts +22 -0
  223. package/dist/tasks/change-set.d.ts.map +1 -1
  224. package/dist/tasks/change-set.js +119 -2
  225. package/dist/tasks/change-set.js.map +1 -1
  226. package/dist/tasks/complete.d.ts.map +1 -1
  227. package/dist/tasks/complete.js +7 -0
  228. package/dist/tasks/complete.js.map +1 -1
  229. package/dist/tasks/done-plan.d.ts +5 -0
  230. package/dist/tasks/done-plan.d.ts.map +1 -1
  231. package/dist/tasks/done-plan.js +54 -19
  232. package/dist/tasks/done-plan.js.map +1 -1
  233. package/dist/tasks/engine-wrap.d.ts +4 -0
  234. package/dist/tasks/engine-wrap.d.ts.map +1 -1
  235. package/dist/tasks/engine-wrap.js +20 -4
  236. package/dist/tasks/engine-wrap.js.map +1 -1
  237. package/dist/tasks/evidence-paths.d.ts +11 -0
  238. package/dist/tasks/evidence-paths.d.ts.map +1 -1
  239. package/dist/tasks/evidence-paths.js +12 -0
  240. package/dist/tasks/evidence-paths.js.map +1 -1
  241. package/dist/tools/fs.d.ts +7 -3
  242. package/dist/tools/fs.d.ts.map +1 -1
  243. package/dist/tools/fs.js +23 -8
  244. package/dist/tools/fs.js.map +1 -1
  245. package/dist/worktree/list.d.ts.map +1 -1
  246. package/dist/worktree/list.js +9 -0
  247. package/dist/worktree/list.js.map +1 -1
  248. package/dist/worktree/prune.d.ts.map +1 -1
  249. package/dist/worktree/prune.js +52 -1
  250. package/dist/worktree/prune.js.map +1 -1
  251. package/migrations/drizzle-cleo-project/20261013000000_t13360-migrated-doc-import/migration.sql +61 -0
  252. package/migrations/drizzle-cleo-project/20261014000000_t13362-docs-publications-ledger/migration.sql +45 -0
  253. package/package.json +13 -13
  254. package/templates/CLEO-INJECTION.md +1 -1
  255. 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"}