@cleocode/contracts 2026.10.4 → 2026.10.5

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 (90) hide show
  1. package/dist/__tests__/cloud-replica-retirement.test.d.ts +10 -0
  2. package/dist/__tests__/cloud-replica-retirement.test.d.ts.map +1 -0
  3. package/dist/__tests__/cloud-replica-retirement.test.js +111 -0
  4. package/dist/__tests__/cloud-replica-retirement.test.js.map +1 -0
  5. package/dist/__tests__/cloud-status-checkpoint-reads.test.d.ts +10 -0
  6. package/dist/__tests__/cloud-status-checkpoint-reads.test.d.ts.map +1 -0
  7. package/dist/__tests__/cloud-status-checkpoint-reads.test.js +49 -0
  8. package/dist/__tests__/cloud-status-checkpoint-reads.test.js.map +1 -0
  9. package/dist/agent-registry.d.ts +5 -1
  10. package/dist/agent-registry.d.ts.map +1 -1
  11. package/dist/cloud/api.d.ts +156 -0
  12. package/dist/cloud/api.d.ts.map +1 -1
  13. package/dist/cloud/api.js +99 -1
  14. package/dist/cloud/api.js.map +1 -1
  15. package/dist/cloud/envelope.d.ts +3 -3
  16. package/dist/dispatch/operations-registry.js +1 -1
  17. package/dist/dispatch/operations-registry.js.map +1 -1
  18. package/dist/doctor.d.ts +76 -3
  19. package/dist/doctor.d.ts.map +1 -1
  20. package/dist/index.d.ts +7 -6
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +2 -2
  23. package/dist/index.js.map +1 -1
  24. package/dist/invariants/sync-write-invariants.d.ts.map +1 -1
  25. package/dist/invariants/sync-write-invariants.js +31 -6
  26. package/dist/invariants/sync-write-invariants.js.map +1 -1
  27. package/dist/ledger/index.d.ts +211 -0
  28. package/dist/ledger/index.d.ts.map +1 -0
  29. package/dist/ledger/index.js +122 -0
  30. package/dist/ledger/index.js.map +1 -0
  31. package/dist/nexus-account.d.ts +9 -0
  32. package/dist/nexus-account.d.ts.map +1 -1
  33. package/dist/nexus-cloud.d.ts +171 -0
  34. package/dist/nexus-cloud.d.ts.map +1 -1
  35. package/dist/nexus-cloud.js +10 -0
  36. package/dist/nexus-cloud.js.map +1 -1
  37. package/dist/nexus-vault.d.ts +113 -1
  38. package/dist/nexus-vault.d.ts.map +1 -1
  39. package/dist/nexus-vault.js +27 -0
  40. package/dist/nexus-vault.js.map +1 -1
  41. package/dist/operations/admin.d.ts +14 -1
  42. package/dist/operations/admin.d.ts.map +1 -1
  43. package/dist/operations/docs-lifecycle.d.ts +23 -0
  44. package/dist/operations/docs-lifecycle.d.ts.map +1 -0
  45. package/dist/operations/docs-lifecycle.js +29 -0
  46. package/dist/operations/docs-lifecycle.js.map +1 -0
  47. package/dist/operations/docs.d.ts +2 -11
  48. package/dist/operations/docs.d.ts.map +1 -1
  49. package/dist/operations/docs.js +3 -17
  50. package/dist/operations/docs.js.map +1 -1
  51. package/dist/project-context.d.ts +7 -0
  52. package/dist/project-context.d.ts.map +1 -1
  53. package/dist/release/evidence-atoms.d.ts +15 -7
  54. package/dist/release/evidence-atoms.d.ts.map +1 -1
  55. package/dist/release/evidence-atoms.js +16 -12
  56. package/dist/release/evidence-atoms.js.map +1 -1
  57. package/dist/resource-governor.d.ts +39 -0
  58. package/dist/resource-governor.d.ts.map +1 -1
  59. package/dist/resource-governor.js.map +1 -1
  60. package/dist/row-identity.d.ts +81 -0
  61. package/dist/row-identity.d.ts.map +1 -1
  62. package/dist/store-restore.d.ts +66 -0
  63. package/dist/store-restore.d.ts.map +1 -0
  64. package/dist/store-restore.js +9 -0
  65. package/dist/store-restore.js.map +1 -0
  66. package/dist/task.d.ts +6 -0
  67. package/dist/task.d.ts.map +1 -1
  68. package/package.json +6 -2
  69. package/src/__tests__/cloud-replica-retirement.test.ts +135 -0
  70. package/src/__tests__/cloud-status-checkpoint-reads.test.ts +60 -0
  71. package/src/agent-registry.ts +5 -1
  72. package/src/cloud/api.ts +125 -1
  73. package/src/dispatch/operations-registry.ts +1 -1
  74. package/src/doctor.ts +82 -3
  75. package/src/index.ts +33 -0
  76. package/src/invariants/sync-write-invariants.ts +81 -42
  77. package/src/ledger/index.ts +141 -0
  78. package/src/nexus-account.ts +9 -0
  79. package/src/nexus-cloud.ts +186 -0
  80. package/src/nexus-vault.ts +124 -1
  81. package/src/operations/admin.ts +14 -1
  82. package/src/operations/conduit.ts +1 -1
  83. package/src/operations/docs-lifecycle.ts +31 -0
  84. package/src/operations/docs.ts +4 -19
  85. package/src/project-context.ts +7 -0
  86. package/src/release/evidence-atoms.ts +16 -12
  87. package/src/resource-governor.ts +40 -0
  88. package/src/row-identity.ts +98 -0
  89. package/src/store-restore.ts +70 -0
  90. package/src/task.ts +6 -0
@@ -119,6 +119,34 @@ function rulePending(task: string): SyncWriteInvariantPending {
119
119
  return { task, reason: 'the typed merge-rule registry does not define it yet (T12344 item)' };
120
120
  }
121
121
 
122
+ /** Where every implemented typed merge rule is enforced at apply (T12344). */
123
+ const MERGE_ENGINE_GATE = {
124
+ module: 'packages/core/src/store/sync/merge/engine.ts',
125
+ functionName: 'applyOp',
126
+ } as const;
127
+
128
+ /**
129
+ * A typed merge rule the merge-rule registry defines (`SYNC_MERGE_RULES`),
130
+ * enforced by the merge engine on every applied op: no longer pending.
131
+ */
132
+ function implemented(entry: SyncWriteInvariant): SyncWriteInvariant {
133
+ const { pending: _pending, ...rest } = entry;
134
+ return { ...rest, runtimeGate: MERGE_ENGINE_GATE };
135
+ }
136
+
137
+ /** A post-apply check the apply runs (T12344 PR-5): no longer pending. */
138
+ function implementedCheck(entry: SyncWriteInvariant, functionName: string): SyncWriteInvariant {
139
+ const { pending: _pending, ...rest } = entry;
140
+ return {
141
+ ...rest,
142
+ check: {
143
+ module: 'packages/core/src/store/sync/apply/post-apply.ts',
144
+ functionName,
145
+ footprint: entry.check?.footprint ?? [],
146
+ },
147
+ };
148
+ }
149
+
122
150
  function trigger(
123
151
  id: string,
124
152
  tables: readonly string[],
@@ -256,13 +284,16 @@ export const SYNC_WRITE_INVARIANTS: readonly SyncWriteInvariant[] = Object.freez
256
284
  ),
257
285
 
258
286
  // §3.6.5: post-apply check families (T12344 items).
259
- pac(
260
- 'task.tree.shape',
261
- 'PAC-01',
262
- 'T12922',
263
- ['tasks_tasks'],
264
- ['rows with a changed parent_id or type', 'their direct children and ancestor chain'],
265
- 'saga→epic→task→subtask matrix, non-saga has a parent, depth ≤ 3, no self-parent',
287
+ implementedCheck(
288
+ pac(
289
+ 'task.tree.shape',
290
+ 'PAC-01',
291
+ 'T12922',
292
+ ['tasks_tasks'],
293
+ ['rows with a changed parent_id or type', 'their direct children and ancestor chain'],
294
+ 'saga→epic→task→subtask matrix, non-saga has a parent, depth ≤ 3, no self-parent',
295
+ ),
296
+ 'checkTaskTreeShape',
266
297
  ),
267
298
  pac(
268
299
  'task.terminal-parent.live-children',
@@ -272,13 +303,12 @@ export const SYNC_WRITE_INVARIANTS: readonly SyncWriteInvariant[] = Object.freez
272
303
  ['rows whose status, stage or parent changed', 'their parent and siblings'],
273
304
  'no terminal parent with a live child; epic stage ≥ children; rollup re-evaluated',
274
305
  ),
275
- pac(
306
+ trigger(
276
307
  'task.dependency.graph',
277
- 'PAC-03',
278
- 'T12924',
279
308
  ['tasks_task_dependencies', 'tasks_tasks'],
280
- ['dependency edges inserted in the page', 'reachability from each depends_on'],
281
- 'dependencies are acyclic, with no self-edge',
309
+ ['tasks_task_dependencies_cycle_guard_insert', 'tasks_task_dependencies_cycle_guard_update'],
310
+ ['PAC-03'],
311
+ 'dependencies are acyclic, with no self-edge: enforced by the T12886 cycle guards on apply (a guard conflict)',
282
312
  ),
283
313
  pac(
284
314
  'task.done.evidence',
@@ -368,39 +398,48 @@ export const SYNC_WRITE_INVARIANTS: readonly SyncWriteInvariant[] = Object.freez
368
398
  ['the owning rows'],
369
399
  'derived counters and cross-column facts agree with their rows',
370
400
  ),
371
- pac(
372
- 'apply.preconditions',
373
- 'PAC-15',
374
- 'T12936',
375
- ['tasks_tasks'],
376
- ['the whole page'],
377
- 'no apply while the local twin collapse has failed; restore is a re-baseline',
401
+ implementedCheck(
402
+ pac(
403
+ 'apply.preconditions',
404
+ 'PAC-15',
405
+ 'T12936',
406
+ ['tasks_tasks'],
407
+ ['the whole page'],
408
+ 'no apply while the local twin collapse has failed; restore is a re-baseline',
409
+ ),
410
+ 'checkApplyPreconditions',
378
411
  ),
379
412
 
380
413
  // §3.6.6: typed merge rules (T12344).
381
- rule(
382
- 'task.status.absorbing',
383
- 'T12937',
384
- 'tasks_tasks',
385
- ['status', 'completed_at', 'cancelled_at', 'cancellation_reason', 'pipeline_stage'],
386
- ['V18', 'V20', 'C22', 'P40', 'P46', 'P47'],
387
- 'done, cancelled and archived are absorbing; leaving them needs an explicit op',
388
- ),
389
- rule(
390
- 'task.pipeline-stage.max',
391
- 'T12938',
392
- 'tasks_tasks',
393
- ['pipeline_stage'],
394
- ['V21', 'C20', 'P01', 'F3'],
395
- 'pipeline_stage is the maximum by STAGE_ORDER, except an explicit restore',
396
- ),
397
- rule(
398
- 'task.verification.frozen-on-done',
399
- 'T12939',
400
- 'tasks_tasks',
401
- ['verification_json'],
402
- ['C01', 'C02', 'C03', 'C17'],
403
- 'verification is frozen once done; otherwise merged per gate',
414
+ implemented(
415
+ rule(
416
+ 'task.status.absorbing',
417
+ 'T12937',
418
+ 'tasks_tasks',
419
+ ['status', 'completed_at', 'cancelled_at', 'cancellation_reason', 'pipeline_stage'],
420
+ ['V18', 'V20', 'C22', 'P40', 'P46', 'P47'],
421
+ 'done, cancelled and archived are absorbing; leaving them needs an explicit op',
422
+ ),
423
+ ),
424
+ implemented(
425
+ rule(
426
+ 'task.pipeline-stage.max',
427
+ 'T12938',
428
+ 'tasks_tasks',
429
+ ['pipeline_stage'],
430
+ ['V21', 'C20', 'P01', 'F3'],
431
+ 'pipeline_stage is the maximum by STAGE_ORDER, except an explicit restore',
432
+ ),
433
+ ),
434
+ implemented(
435
+ rule(
436
+ 'task.verification.frozen-on-done',
437
+ 'T12939',
438
+ 'tasks_tasks',
439
+ ['verification_json'],
440
+ ['C01', 'C02', 'C03', 'C17'],
441
+ 'verification is frozen once done; otherwise merged per gate',
442
+ ),
404
443
  ),
405
444
  rule(
406
445
  'session.status.terminal',
@@ -0,0 +1,141 @@
1
+ /**
2
+ * `@cleocode/contracts/ledger`: the decoded change-journal transaction and op
3
+ * shapes (journal spec `t12342-t12343-journal-design` §2.6).
4
+ *
5
+ * A sealed segment carries an array of {@link LedgerTxn}; each transaction
6
+ * carries {@link LedgerOp}s on sync-set rows, keyed by row uid. Values use the
7
+ * canonical typed wire encoding ({@link LedgerWireValue}), and counter deltas
8
+ * travel as `{ $inc }` ({@link LedgerValue}).
9
+ *
10
+ * Zod schemas, inferred types and const data only (arch gate 10). The sealer
11
+ * that produces these shapes and the merge engine that applies them live in
12
+ * `@cleocode/core` (`store/sync/`).
13
+ *
14
+ * @module ledger
15
+ * @task T12344
16
+ * @epic T12323
17
+ */
18
+
19
+ import { z } from 'zod';
20
+ import { Hlc } from '../cloud/ids.js';
21
+
22
+ /** The transaction format version this build writes and reads (`LedgerTxn.v`). */
23
+ export const LEDGER_TXN_VERSION = 1;
24
+
25
+ /**
26
+ * A typed wire value: a string, null, a safe integer, or a typed escape for an
27
+ * integer beyond 2^53 (`$i`), a REAL (`$r`, `%!.17g` text, `Inf` accepted) or a
28
+ * BLOB (`$b`, base64).
29
+ */
30
+ export const LedgerWireValue = z.union([
31
+ z.string(),
32
+ z.number(),
33
+ z.null(),
34
+ z.object({ $i: z.string() }).strict(),
35
+ z.object({ $r: z.string() }).strict(),
36
+ z.object({ $b: z.string() }).strict(),
37
+ ]);
38
+ export type LedgerWireValue = z.infer<typeof LedgerWireValue>;
39
+
40
+ /** A counter delta (`SYNC_COUNTER_COLUMNS`): applied by summing, never by plain LWW. */
41
+ export const LedgerIncrement = z.object({ $inc: z.number() }).strict();
42
+ export type LedgerIncrement = z.infer<typeof LedgerIncrement>;
43
+
44
+ /** An after-value: a wire value or a counter delta. */
45
+ export const LedgerValue = z.union([LedgerWireValue, LedgerIncrement]);
46
+ export type LedgerValue = z.infer<typeof LedgerValue>;
47
+
48
+ /** The op kinds: insert, update, delete, re-key. */
49
+ export const LedgerOpKind = z.enum(['I', 'U', 'D', 'K']);
50
+ export type LedgerOpKind = z.infer<typeof LedgerOpKind>;
51
+
52
+ /** One op on one sync-set row (§2.6). */
53
+ export const LedgerOp = z
54
+ .object({
55
+ /** Table. */
56
+ t: z.string().min(1),
57
+ /** Row uid; for K the OLD uid. */
58
+ u: z.string().min(1),
59
+ o: LedgerOpKind,
60
+ /** The op's HLC; also every field's HLC unless `fh` names a different one. */
61
+ h: Hlc,
62
+ /** K only: the new uid. */
63
+ nu: z.string().min(1).optional(),
64
+ /** birth_fp, on every op of a minted row; for K, the new value. */
65
+ bfp: z.string().optional(),
66
+ /** K only: the old birth_fp. */
67
+ obfp: z.string().optional(),
68
+ /** Per-field HLCs where they differ from `h`. */
69
+ fh: z.record(z.string(), Hlc).optional(),
70
+ /** Natural rows: the key, with refs as uids. */
71
+ k: z.record(z.string(), LedgerValue).optional(),
72
+ /** After-values (refs as uids; counters as `{ $inc }`). */
73
+ a: z.record(z.string(), LedgerValue).optional(),
74
+ /** Before-image (secrets excluded). */
75
+ b: z.record(z.string(), LedgerWireValue).optional(),
76
+ })
77
+ .strict()
78
+ .refine((op) => (op.o === 'K') === (op.nu !== undefined), {
79
+ message: 'nu is required on a K op and only there',
80
+ });
81
+ export type LedgerOp = z.infer<typeof LedgerOp>;
82
+
83
+ /** Who wrote a transaction; `op` names the command (e.g. `tasks.reopen`). */
84
+ export const LedgerActor = z
85
+ .object({
86
+ agent: z.string().optional(),
87
+ session: z.string().optional(),
88
+ op: z.string().optional(),
89
+ })
90
+ .strict();
91
+ export type LedgerActor = z.infer<typeof LedgerActor>;
92
+
93
+ /** How a transaction's writes reached the store. */
94
+ export const LedgerTxnVia = z.enum(['accessor', 'connection', 'foreign', 'repair', 'rebind']);
95
+ export type LedgerTxnVia = z.infer<typeof LedgerTxnVia>;
96
+
97
+ /** What a transaction is for. */
98
+ export const LedgerTxnKind = z.enum([
99
+ 'write',
100
+ 'remint',
101
+ 'rekey',
102
+ 'repair',
103
+ 'exodus',
104
+ 'import',
105
+ 'retire',
106
+ ]);
107
+ export type LedgerTxnKind = z.infer<typeof LedgerTxnKind>;
108
+
109
+ /**
110
+ * One sealed transaction (§2.6). `v` is checked by the receiver, not by this
111
+ * schema, so a newer format is refused with an explicit error rather than a
112
+ * parse failure.
113
+ */
114
+ export const LedgerTxn = z
115
+ .object({
116
+ v: z.number().int().positive(),
117
+ /** `${replicaId}:${localTxnSeq}`. */
118
+ txn: z.string().min(1),
119
+ /** Only for split oversized groups: [part, of]. */
120
+ part: z.tuple([z.number().int().positive(), z.number().int().positive()]).optional(),
121
+ hlc: Hlc,
122
+ /** Merge key is (project, uid). */
123
+ project: z.string().nullable(),
124
+ scope: z.enum(['project', 'global']),
125
+ via: LedgerTxnVia,
126
+ kind: LedgerTxnKind,
127
+ actor: LedgerActor.nullable(),
128
+ ops: z.array(LedgerOp),
129
+ retire: z
130
+ .object({
131
+ replica: z.string(),
132
+ successor: z.string(),
133
+ lastReplicaSeq: z.number().int().nonnegative(),
134
+ })
135
+ .strict()
136
+ .optional(),
137
+ /** Ed25519 over the transaction signing message (§2.8). */
138
+ sig: z.string(),
139
+ })
140
+ .strict();
141
+ export type LedgerTxn = z.infer<typeof LedgerTxn>;
@@ -410,6 +410,15 @@ export interface NexusProjectLinkResult {
410
410
  * text is also in {@link NexusProjectLinkResult.warnings}.
411
411
  */
412
412
  attachError: { code: string; message: string; fix: string | null } | null;
413
+ /**
414
+ * The project data key version the server holds from this registration
415
+ * (onboarding B): `1` when the link sent a new project's first key with it,
416
+ * `null` when it sent none (the project already existed, the server does
417
+ * not take keys at registration, or this device has no account key; see
418
+ * {@link NexusProjectLinkResult.warnings}). With `null`, the first
419
+ * `cleo cloud push` creates the key.
420
+ */
421
+ initialKeyVersion: number | null;
413
422
  /** Non-fatal problems (for example, presence could not be sent). */
414
423
  warnings: string[];
415
424
  }
@@ -264,10 +264,21 @@ export const nexusCloudStatusCheckSchema = z.object({
264
264
  detail: z.string(),
265
265
  });
266
266
 
267
+ /**
268
+ * The E3 `features` entry of a server whose `POST /v1/projects` stores `initialKey`, a new project's
269
+ * first data key (onboarding B, cleo-nexus T095). `cleo project link` sends a key only to such a server.
270
+ */
271
+ export const NEXUS_FEATURE_PROJECT_INITIAL_KEY = 'project.initial-key';
272
+
267
273
  /** `NexusStatus` (E3 `GET /v1/status`). */
268
274
  export const nexusCloudStatusSchema = z.object({
269
275
  serverTime: isoTime.optional(),
270
276
  apiVersion: z.string().optional(),
277
+ /**
278
+ * Optional server features (e.g. {@link NEXUS_FEATURE_PROJECT_INITIAL_KEY}). Absent from servers older
279
+ * than the list, which therefore support none of them.
280
+ */
281
+ features: z.array(z.string()).optional(),
271
282
  user: nexusCloudUserSchema,
272
283
  credential: nexusCloudCredentialSchema,
273
284
  device: nexusCloudDeviceSchema.nullable(),
@@ -393,6 +404,23 @@ export interface CloudProjectsResult {
393
404
  warnings: CloudWarning[];
394
405
  }
395
406
 
407
+ /**
408
+ * A replica this device retired: the store file it named was replaced (a
409
+ * vault restore or pull) or rolled back, so a new replica took its place
410
+ * (journal spec §1.5). The server lists it as history until S4's signed
411
+ * `retire` transaction announces it (T13109).
412
+ */
413
+ export interface CloudRetiredReplica {
414
+ /** The retired replica id. */
415
+ replicaId: string;
416
+ /** The replica that replaced it. */
417
+ successor: string;
418
+ /** When it was retired on this device. */
419
+ retiredAt: string;
420
+ /** The rebind reason(s), e.g. `vault-restore`; `null` when not recorded. */
421
+ reason: string | null;
422
+ }
423
+
396
424
  /** `cleo cloud projects show [<id>]` (operation `cloud.projects.show`). */
397
425
  export interface CloudProjectShowResult extends NexusCloudProjectDetail {
398
426
  /** API origin asked. */
@@ -406,6 +434,8 @@ export interface CloudProjectShowResult extends NexusCloudProjectDetail {
406
434
  * fetched from E15 and `replicas` holds them all (up to the page budget).
407
435
  */
408
436
  replicaPaging: CloudPaging;
437
+ /** Listed replicas this device retired (from its local registry), newest first. */
438
+ retiredHere: CloudRetiredReplica[];
409
439
  /** Non-fatal problems. */
410
440
  warnings: CloudWarning[];
411
441
  }
@@ -448,6 +478,11 @@ export interface CloudStatusLocal {
448
478
  projectId: string | null;
449
479
  /** The active replica id of the current project's store (read-only; never bound here). */
450
480
  replicaId: string | null;
481
+ /**
482
+ * Earlier replicas of the current project's store that this device retired
483
+ * (from its local registry), newest first: the server still lists them.
484
+ */
485
+ retiredReplicas: CloudRetiredReplica[];
451
486
  /** `.cleo/nexus-link.json` when it holds an entry for the origin, else `null`. */
452
487
  linkPath: string | null;
453
488
  /** The device credential store (`nexus-device.json`). */
@@ -455,6 +490,25 @@ export interface CloudStatusLocal {
455
490
  }
456
491
 
457
492
  /** `CloudStatusResult` (§4.4): the data of the `cloud.status` envelope. */
493
+ /**
494
+ * One device holding the project, as `cleo cloud status` lists it (T13290):
495
+ * a replica row reduced to who holds it and whether its presence is fresh.
496
+ */
497
+ export interface CloudStatusHolder {
498
+ /** Nexus device id. */
499
+ deviceId: string;
500
+ /** The device's display name. */
501
+ deviceName: string;
502
+ /** The replica this device holds. */
503
+ replicaId: string;
504
+ /** Last presence report, or `null` when it never reported. */
505
+ presenceAt: string | null;
506
+ /** Presence within {@link NEXUS_PRESENCE_FRESH_SECONDS}. */
507
+ fresh: boolean;
508
+ /** This machine's own device. */
509
+ thisDevice: boolean;
510
+ }
511
+
458
512
  export interface CloudStatusResult {
459
513
  /** The remote verdict, downgraded by local facts the server cannot see. */
460
514
  verdict: CloudStatusVerdict;
@@ -466,10 +520,140 @@ export interface CloudStatusResult {
466
520
  remote: NexusCloudStatus | null;
467
521
  /** This device's global store on the account's home stream (T12952); absent when not signed in. */
468
522
  global?: CloudStatusGlobalStore;
523
+ /**
524
+ * The local sync journal of each store this machine holds (T12998): the
525
+ * project store and the global store. Read on every path (signed in or not,
526
+ * online or offline); absent when no store could be read.
527
+ */
528
+ sync?: CloudStatusSync;
529
+ /**
530
+ * The devices holding the project (T13290), from the replica list; absent
531
+ * when there is no registered project to ask about or the list failed (a
532
+ * warning says so). Additive: older readers ignore it.
533
+ */
534
+ holders?: CloudStatusHolder[];
469
535
  /** Non-fatal problems. */
470
536
  warnings: CloudWarning[];
471
537
  }
472
538
 
539
+ /**
540
+ * A sync fact this build cannot know yet, and why (T12998). The block's shape
541
+ * is fixed now, so the fields keep their names when the outbox (T12343) and
542
+ * segment push (S4) fill them in.
543
+ */
544
+ export interface CloudSyncUnknown {
545
+ readonly known: false;
546
+ /** What is missing: the transactional outbox (T12343) or segment push/pull (S4). */
547
+ readonly needs: 'T12343' | 'S4';
548
+ /** One sentence for the operator. */
549
+ readonly reason: string;
550
+ }
551
+
552
+ /** A sync fact this build knows. */
553
+ export interface CloudSyncKnown<T> {
554
+ readonly known: true;
555
+ readonly value: T;
556
+ }
557
+
558
+ /** A sync fact: known, or unknown with the reason (T12998). */
559
+ export type CloudSyncFact<T> = CloudSyncKnown<T> | CloudSyncUnknown;
560
+
561
+ /** The sync flags of a store (`_sync_meta`; all off when the journal is absent). */
562
+ export interface CloudSyncFlags {
563
+ readonly capture: boolean;
564
+ readonly seal: boolean;
565
+ readonly push: boolean;
566
+ readonly pull: boolean;
567
+ readonly strict: boolean;
568
+ }
569
+
570
+ /** How far a stream lags the server. */
571
+ export interface CloudSyncLag {
572
+ /** Ops on the server this store has not pulled, plus ops here not yet pushed. */
573
+ readonly ops: number;
574
+ /** Age of the oldest op not yet exchanged, in milliseconds. */
575
+ readonly ms: number;
576
+ }
577
+
578
+ /** One store's local sync journal, as `cleo cloud status` reports it (T12998). Read-only. */
579
+ /** A local write a sync rebase holds unreplayed until its echo is decided (journal spec §3.5 Rule 5). */
580
+ export interface CloudStatusHeldRow {
581
+ /** The table it writes. */
582
+ readonly table: string;
583
+ /** The row's uid. */
584
+ readonly uid: string;
585
+ /** The local transaction it belongs to. */
586
+ readonly txn: string;
587
+ /** When it was held (ISO-8601). */
588
+ readonly heldAt: string;
589
+ /** What refused its replay: the conflict preview. */
590
+ readonly reason: string;
591
+ }
592
+
593
+ /** `CloudStatusSyncStream.held`: the store's held writes (§3.5 Rule 5). */
594
+ export interface CloudStatusHeld {
595
+ /** How many local ops are held. */
596
+ readonly count: number;
597
+ /** When the oldest was held (ISO-8601), or `null` when none is. */
598
+ readonly oldestAt: string | null;
599
+ /** Holds older than `warnDays`, oldest first (`cleo doctor sync-journal` lists the same). */
600
+ readonly long: readonly CloudStatusHeldRow[];
601
+ /** The age, in days, past which a hold is long. */
602
+ readonly warnDays: number;
603
+ }
604
+
605
+ export interface CloudStatusSyncStream {
606
+ /** Which store. */
607
+ readonly scope: 'project' | 'global';
608
+ /** Its stream (`project:<id>`, `home:<userId>`) when known locally, else `null`. */
609
+ readonly stream: string | null;
610
+ /** The store file. */
611
+ readonly dbPath: string;
612
+ /** The sync journal tables are installed in this store. */
613
+ readonly journalInstalled: boolean;
614
+ /** The store's sync flags. */
615
+ readonly flags: CloudSyncFlags;
616
+ /** Captured changes not yet sealed into ops (`_sync_capture` live rows). */
617
+ readonly unsealedOps: number;
618
+ /** When the oldest unsealed change was captured (epoch ms), or `null`. */
619
+ readonly oldestUnsealedAtMs: number | null;
620
+ /** The highest sealed local sequence (`_sync_txn.local_seq`), or `null` before the first seal. */
621
+ readonly lastSealedSeq: number | null;
622
+ /** Captures the sealer could not read and holds aside (`_sync_quarantine`), per table. */
623
+ readonly quarantined: Readonly<Record<string, number>>;
624
+ /** Tables marked suspect until a repair re-emits them. */
625
+ readonly suspectTables: readonly string[];
626
+ /** Local writes a sync rebase holds, until their echo is decided (§3.5 Rule 5). */
627
+ readonly held: CloudStatusHeld;
628
+ /** Sealed ops not yet handed to the outbox for sending. */
629
+ readonly unsentOps: CloudSyncFact<number>;
630
+ /** The last sequence this store pushed to the server. */
631
+ readonly lastPushedSeq: CloudSyncFact<number>;
632
+ /** The last server sequence this store pulled. */
633
+ readonly lastPulledSeq: CloudSyncFact<number>;
634
+ /** The server's head sequence for this stream. */
635
+ readonly serverHeadSeq: CloudSyncFact<number>;
636
+ /** Devices attached to this stream and when each last synced. */
637
+ readonly devices: CloudSyncFact<
638
+ ReadonlyArray<{ readonly deviceId: string; readonly lastSyncAt: string | null }>
639
+ >;
640
+ /** Conflicts held open on this stream. */
641
+ readonly openConflicts: CloudSyncFact<number>;
642
+ /** How far this store lags the server. */
643
+ readonly lag: CloudSyncFact<CloudSyncLag>;
644
+ }
645
+
646
+ /** `CloudStatusResult.sync` (T12998). */
647
+ export interface CloudStatusSync {
648
+ /** One entry per store this machine holds (project, then global), when its file exists. */
649
+ readonly streams: readonly CloudStatusSyncStream[];
650
+ /**
651
+ * This build reads the local journal only; the server-side fields stay
652
+ * unknown until the outbox (T12343) and segment push/pull (S4) land.
653
+ */
654
+ readonly partial: true;
655
+ }
656
+
473
657
  /** `cleo cloud status`: this device's global store (the main brain) on Cleo Nexus (T12952). */
474
658
  export interface CloudStatusGlobalStore {
475
659
  /** The server lists home-stream replicas. */
@@ -492,6 +676,8 @@ export interface CloudStatusOfflineDetails {
492
676
  summary: CloudStatusSummary;
493
677
  /** Warnings collected before the server stopped answering. */
494
678
  warnings: CloudWarning[];
679
+ /** The local sync journal, read before the server was asked (T12998). */
680
+ sync?: CloudStatusSync;
495
681
  }
496
682
 
497
683
  // ---------- error mapping (§4.0.4) ----------