@cleocode/contracts 2026.10.4 → 2026.10.5-rc.ci.672
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/__tests__/cloud-replica-retirement.test.d.ts +10 -0
- package/dist/__tests__/cloud-replica-retirement.test.d.ts.map +1 -0
- package/dist/__tests__/cloud-replica-retirement.test.js +111 -0
- package/dist/__tests__/cloud-replica-retirement.test.js.map +1 -0
- package/dist/__tests__/cloud-status-checkpoint-reads.test.d.ts +10 -0
- package/dist/__tests__/cloud-status-checkpoint-reads.test.d.ts.map +1 -0
- package/dist/__tests__/cloud-status-checkpoint-reads.test.js +49 -0
- package/dist/__tests__/cloud-status-checkpoint-reads.test.js.map +1 -0
- package/dist/agent-registry.d.ts +5 -1
- package/dist/agent-registry.d.ts.map +1 -1
- package/dist/cloud/api.d.ts +156 -0
- package/dist/cloud/api.d.ts.map +1 -1
- package/dist/cloud/api.js +99 -1
- package/dist/cloud/api.js.map +1 -1
- package/dist/cloud/envelope.d.ts +3 -3
- package/dist/dispatch/operations-registry.js +1 -1
- package/dist/dispatch/operations-registry.js.map +1 -1
- package/dist/doctor.d.ts +76 -3
- package/dist/doctor.d.ts.map +1 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/invariants/sync-write-invariants.d.ts.map +1 -1
- package/dist/invariants/sync-write-invariants.js +31 -6
- package/dist/invariants/sync-write-invariants.js.map +1 -1
- package/dist/ledger/index.d.ts +211 -0
- package/dist/ledger/index.d.ts.map +1 -0
- package/dist/ledger/index.js +122 -0
- package/dist/ledger/index.js.map +1 -0
- package/dist/nexus-account.d.ts +9 -0
- package/dist/nexus-account.d.ts.map +1 -1
- package/dist/nexus-cloud.d.ts +171 -0
- package/dist/nexus-cloud.d.ts.map +1 -1
- package/dist/nexus-cloud.js +10 -0
- package/dist/nexus-cloud.js.map +1 -1
- package/dist/nexus-vault.d.ts +113 -1
- package/dist/nexus-vault.d.ts.map +1 -1
- package/dist/nexus-vault.js +27 -0
- package/dist/nexus-vault.js.map +1 -1
- package/dist/operations/admin.d.ts +14 -1
- package/dist/operations/admin.d.ts.map +1 -1
- package/dist/operations/docs-lifecycle.d.ts +23 -0
- package/dist/operations/docs-lifecycle.d.ts.map +1 -0
- package/dist/operations/docs-lifecycle.js +29 -0
- package/dist/operations/docs-lifecycle.js.map +1 -0
- package/dist/operations/docs.d.ts +2 -11
- package/dist/operations/docs.d.ts.map +1 -1
- package/dist/operations/docs.js +3 -17
- package/dist/operations/docs.js.map +1 -1
- package/dist/project-context.d.ts +7 -0
- package/dist/project-context.d.ts.map +1 -1
- package/dist/release/evidence-atoms.d.ts +15 -7
- package/dist/release/evidence-atoms.d.ts.map +1 -1
- package/dist/release/evidence-atoms.js +16 -12
- package/dist/release/evidence-atoms.js.map +1 -1
- package/dist/resource-governor.d.ts +39 -0
- package/dist/resource-governor.d.ts.map +1 -1
- package/dist/resource-governor.js.map +1 -1
- package/dist/row-identity.d.ts +81 -0
- package/dist/row-identity.d.ts.map +1 -1
- package/dist/store-restore.d.ts +66 -0
- package/dist/store-restore.d.ts.map +1 -0
- package/dist/store-restore.js +9 -0
- package/dist/store-restore.js.map +1 -0
- package/dist/task.d.ts +6 -0
- package/dist/task.d.ts.map +1 -1
- package/package.json +6 -2
- package/src/__tests__/cloud-replica-retirement.test.ts +135 -0
- package/src/__tests__/cloud-status-checkpoint-reads.test.ts +60 -0
- package/src/agent-registry.ts +5 -1
- package/src/cloud/api.ts +125 -1
- package/src/dispatch/operations-registry.ts +1 -1
- package/src/doctor.ts +82 -3
- package/src/index.ts +33 -0
- package/src/invariants/sync-write-invariants.ts +81 -42
- package/src/ledger/index.ts +141 -0
- package/src/nexus-account.ts +9 -0
- package/src/nexus-cloud.ts +186 -0
- package/src/nexus-vault.ts +124 -1
- package/src/operations/admin.ts +14 -1
- package/src/operations/conduit.ts +1 -1
- package/src/operations/docs-lifecycle.ts +31 -0
- package/src/operations/docs.ts +4 -19
- package/src/project-context.ts +7 -0
- package/src/release/evidence-atoms.ts +16 -12
- package/src/resource-governor.ts +40 -0
- package/src/row-identity.ts +98 -0
- package/src/store-restore.ts +70 -0
- package/src/task.ts +6 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud contract mirror of cleo-nexus #41 (T123 replica retirement, contract
|
|
3
|
+
* v2.28; T13284): the E31 request and answer, the stored retirement, and the
|
|
4
|
+
* `retiredAt` / `successor` fields home replicas gain, as the server validates
|
|
5
|
+
* them.
|
|
6
|
+
*
|
|
7
|
+
* @task T13284
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it } from 'vitest';
|
|
11
|
+
import {
|
|
12
|
+
AttachHomeReplicaResult,
|
|
13
|
+
HomeReplica,
|
|
14
|
+
ListHomeReplicasResult,
|
|
15
|
+
ReplicaRetirement,
|
|
16
|
+
RetireReplicaRequest,
|
|
17
|
+
RetireReplicaResult,
|
|
18
|
+
} from '../cloud/index.js';
|
|
19
|
+
import { NEXUS_REPLICA_RETIREMENT_REASONS } from '../nexus-vault.js';
|
|
20
|
+
|
|
21
|
+
const DEVICE = '0192f1c2-7d3e-4abc-8def-0000000000b0';
|
|
22
|
+
const R1 = '0192f1c2-7d3e-7abc-8def-0000000000a1';
|
|
23
|
+
const R2 = '0192f1c2-7d3e-7abc-8def-0000000000b1';
|
|
24
|
+
const STREAM = 'project:0192f1c2-7d3e-7abc-8def-0123456789ab';
|
|
25
|
+
const SIG = Buffer.alloc(64, 7).toString('base64');
|
|
26
|
+
|
|
27
|
+
const request = {
|
|
28
|
+
deviceId: DEVICE,
|
|
29
|
+
successor: R2,
|
|
30
|
+
lastReplicaSeq: 4,
|
|
31
|
+
txnId: `${R1}:17`,
|
|
32
|
+
signature: SIG,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
describe('RetireReplicaRequest (T13284)', () => {
|
|
36
|
+
it('takes a successor, lastReplicaSeq and txn id, each nullable', () => {
|
|
37
|
+
expect(RetireReplicaRequest.safeParse(request).success).toBe(true);
|
|
38
|
+
expect(
|
|
39
|
+
RetireReplicaRequest.safeParse({
|
|
40
|
+
...request,
|
|
41
|
+
successor: null,
|
|
42
|
+
lastReplicaSeq: null,
|
|
43
|
+
txnId: null,
|
|
44
|
+
}).success,
|
|
45
|
+
).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('refuses a negative or fractional lastReplicaSeq and a malformed txn id', () => {
|
|
49
|
+
expect(RetireReplicaRequest.safeParse({ ...request, lastReplicaSeq: -1 }).success).toBe(false);
|
|
50
|
+
expect(RetireReplicaRequest.safeParse({ ...request, lastReplicaSeq: 1.5 }).success).toBe(false);
|
|
51
|
+
for (const txnId of [
|
|
52
|
+
R1,
|
|
53
|
+
`${R1}:`,
|
|
54
|
+
`${R1}:01`,
|
|
55
|
+
`${R1}:-1`,
|
|
56
|
+
`not-a-uuid:3`,
|
|
57
|
+
`${R1}:12345678901234567`,
|
|
58
|
+
]) {
|
|
59
|
+
expect(RetireReplicaRequest.safeParse({ ...request, txnId }).success).toBe(false);
|
|
60
|
+
}
|
|
61
|
+
// Any UUID may emit the transaction (the successor on a rebind), not only a v7 replica id.
|
|
62
|
+
expect(RetireReplicaRequest.safeParse({ ...request, txnId: `${DEVICE}:0` }).success).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('a successor must be a replica id (UUIDv7)', () => {
|
|
66
|
+
expect(RetireReplicaRequest.safeParse({ ...request, successor: DEVICE }).success).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('ReplicaRetirement and RetireReplicaResult (T13284)', () => {
|
|
71
|
+
const stored = {
|
|
72
|
+
streamId: STREAM,
|
|
73
|
+
replicaId: R1,
|
|
74
|
+
successor: R2,
|
|
75
|
+
lastReplicaSeq: null,
|
|
76
|
+
signerDeviceId: DEVICE,
|
|
77
|
+
txnId: null,
|
|
78
|
+
signature: SIG,
|
|
79
|
+
retiredAt: '2026-10-06T07:00:00.000Z',
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
it('is the stored record, wrapped as { retirement }', () => {
|
|
83
|
+
expect(ReplicaRetirement.safeParse(stored).success).toBe(true);
|
|
84
|
+
expect(RetireReplicaResult.safeParse({ retirement: stored }).success).toBe(true);
|
|
85
|
+
expect(RetireReplicaResult.safeParse(stored).success).toBe(false);
|
|
86
|
+
const { retiredAt: _retiredAt, ...undated } = stored;
|
|
87
|
+
expect(ReplicaRetirement.safeParse(undated).success).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('home replicas carry retiredAt and successor (T13284)', () => {
|
|
92
|
+
const replica = {
|
|
93
|
+
replicaId: R1,
|
|
94
|
+
deviceId: DEVICE,
|
|
95
|
+
deviceState: 'active',
|
|
96
|
+
attachedAt: '2026-10-01T00:00:00.000Z',
|
|
97
|
+
lastSyncAt: null,
|
|
98
|
+
presence: null,
|
|
99
|
+
presenceAt: null,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
it('parses an older server without them, and a retired replica with them', () => {
|
|
103
|
+
expect(HomeReplica.safeParse(replica).success).toBe(true);
|
|
104
|
+
const retired = { ...replica, retiredAt: '2026-10-06T07:00:00.000Z', successor: R2 };
|
|
105
|
+
expect(HomeReplica.parse(retired)).toMatchObject({
|
|
106
|
+
retiredAt: retired.retiredAt,
|
|
107
|
+
successor: R2,
|
|
108
|
+
});
|
|
109
|
+
expect(HomeReplica.safeParse({ ...replica, retiredAt: null, successor: null }).success).toBe(
|
|
110
|
+
true,
|
|
111
|
+
);
|
|
112
|
+
expect(HomeReplica.safeParse({ ...replica, successor: DEVICE }).success).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('the attach answer and the list follow HomeReplica', () => {
|
|
116
|
+
expect(AttachHomeReplicaResult).toBe(HomeReplica);
|
|
117
|
+
expect(ListHomeReplicasResult.shape.replicas.element).toBe(HomeReplica);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('E31 refusal reasons (T13284)', () => {
|
|
122
|
+
it('are the seven server reasons', () => {
|
|
123
|
+
expect([...NEXUS_REPLICA_RETIREMENT_REASONS].sort()).toEqual(
|
|
124
|
+
[
|
|
125
|
+
'not-pinned-or-owner',
|
|
126
|
+
'replica-retired',
|
|
127
|
+
'retire-below-head',
|
|
128
|
+
'retired',
|
|
129
|
+
'successor-other-user',
|
|
130
|
+
'successor-retired',
|
|
131
|
+
'successor-taken',
|
|
132
|
+
].sort(),
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud contract mirrors of cleo-nexus #38 (`DeviceScope`, `StatusQuery`;
|
|
3
|
+
* T13282) and #39 (`GetCheckpointResult`; T13283): the shapes the server
|
|
4
|
+
* validates, so a client that parses with them agrees with it.
|
|
5
|
+
*
|
|
6
|
+
* @task T13282
|
|
7
|
+
* @task T13283
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it } from 'vitest';
|
|
11
|
+
import {
|
|
12
|
+
DeviceScope,
|
|
13
|
+
GetCheckpointResult,
|
|
14
|
+
ListCheckpointsResult,
|
|
15
|
+
StatusQuery,
|
|
16
|
+
} from '../cloud/index.js';
|
|
17
|
+
|
|
18
|
+
const PROJECT = '0190a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b';
|
|
19
|
+
const REPLICA = '0198a1b2-0000-7000-8000-0000000000a1';
|
|
20
|
+
|
|
21
|
+
describe('DeviceScope and StatusQuery (T13282)', () => {
|
|
22
|
+
it('DeviceScope is the eight §2.3 scopes and nothing else', () => {
|
|
23
|
+
expect(DeviceScope.options).toEqual([
|
|
24
|
+
'account:read',
|
|
25
|
+
'devices:read',
|
|
26
|
+
'projects:read',
|
|
27
|
+
'projects:write',
|
|
28
|
+
'sync:read',
|
|
29
|
+
'sync:write',
|
|
30
|
+
'keys:read',
|
|
31
|
+
'keys:write',
|
|
32
|
+
]);
|
|
33
|
+
expect(DeviceScope.safeParse('admin').success).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('StatusQuery: both optional, replicaId requires projectId and must be a UUIDv7', () => {
|
|
37
|
+
expect(StatusQuery.safeParse({}).success).toBe(true);
|
|
38
|
+
expect(StatusQuery.safeParse({ projectId: PROJECT }).success).toBe(true);
|
|
39
|
+
expect(StatusQuery.safeParse({ projectId: PROJECT, replicaId: REPLICA }).success).toBe(true);
|
|
40
|
+
const orphan = StatusQuery.safeParse({ replicaId: REPLICA });
|
|
41
|
+
expect(orphan.success).toBe(false);
|
|
42
|
+
expect(orphan.error?.issues[0]?.message).toBe('replicaId requires projectId');
|
|
43
|
+
// A UUIDv4 is not a replica id.
|
|
44
|
+
expect(
|
|
45
|
+
StatusQuery.safeParse({
|
|
46
|
+
projectId: PROJECT,
|
|
47
|
+
replicaId: '0198a1b2-0000-4000-8000-0000000000a1',
|
|
48
|
+
}).success,
|
|
49
|
+
).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('GetCheckpointResult (T13283)', () => {
|
|
54
|
+
it('is one checkpoint shaped exactly as a list item', () => {
|
|
55
|
+
const listItem = ListCheckpointsResult.shape.checkpoints.element;
|
|
56
|
+
expect(GetCheckpointResult.shape.checkpoint).toBe(listItem);
|
|
57
|
+
expect(Object.keys(GetCheckpointResult.shape)).toEqual(['checkpoint']);
|
|
58
|
+
expect(GetCheckpointResult.safeParse({ checkpoints: [] }).success).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
});
|
package/src/agent-registry.ts
CHANGED
|
@@ -47,7 +47,11 @@ export interface AgentCredential {
|
|
|
47
47
|
* device). Re-register with `cleo agent register --id <id> --api-key <key>`.
|
|
48
48
|
*/
|
|
49
49
|
requiresReauth?: boolean;
|
|
50
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Base URL of the cloud messaging API, or `'local'` for none. Rows from
|
|
52
|
+
* before T13169 may still hold the retired `https://api.signaldock.io`;
|
|
53
|
+
* CLEO never calls a SignalDock host.
|
|
54
|
+
*/
|
|
51
55
|
apiBaseUrl: string;
|
|
52
56
|
/** Agent classification from the registry (e.g. 'code_dev', 'orchestrator'). */
|
|
53
57
|
classification?: string;
|
package/src/cloud/api.ts
CHANGED
|
@@ -69,8 +69,45 @@ export type Device = z.infer<typeof Device>;
|
|
|
69
69
|
export const ListDevicesResult = z.object({ devices: z.array(Device) });
|
|
70
70
|
export type ListDevicesResult = z.infer<typeof ListDevicesResult>;
|
|
71
71
|
|
|
72
|
+
// ---------- device credential scopes and E3 status (§4.1) ----------
|
|
73
|
+
|
|
74
|
+
/** What a device credential may do (§2.3). A signed-in session holds every scope. */
|
|
75
|
+
export const DeviceScope = z.enum([
|
|
76
|
+
'account:read',
|
|
77
|
+
'devices:read',
|
|
78
|
+
'projects:read',
|
|
79
|
+
'projects:write',
|
|
80
|
+
'sync:read',
|
|
81
|
+
'sync:write',
|
|
82
|
+
'keys:read',
|
|
83
|
+
'keys:write',
|
|
84
|
+
]);
|
|
85
|
+
/** One device credential scope. */
|
|
86
|
+
export type DeviceScope = z.infer<typeof DeviceScope>;
|
|
87
|
+
|
|
88
|
+
/** E3 `GET /v1/status` query (§4.1): `replicaId` (a UUIDv7) requires `projectId`. */
|
|
89
|
+
export const StatusQuery = z
|
|
90
|
+
.object({ projectId: ProjectId.optional(), replicaId: ReplicaId.optional() })
|
|
91
|
+
.refine((q) => q.replicaId === undefined || q.projectId !== undefined, {
|
|
92
|
+
message: 'replicaId requires projectId',
|
|
93
|
+
});
|
|
94
|
+
/** The E3 status query. */
|
|
95
|
+
export type StatusQuery = z.infer<typeof StatusQuery>;
|
|
96
|
+
|
|
72
97
|
// ---------- projects ----------
|
|
73
98
|
|
|
99
|
+
/**
|
|
100
|
+
* A new project's first data key, stored in the same transaction as its registration (onboarding B): key
|
|
101
|
+
* version 1, wrapped by the registrant's account master key. Needs keys:write on top of projects:write, and is
|
|
102
|
+
* honoured only when the call creates the project. Later versions stay rotations
|
|
103
|
+
* (PUT /v1/projects/:projectId/keys/:userId).
|
|
104
|
+
*/
|
|
105
|
+
export const InitialProjectKey = z.object({
|
|
106
|
+
/** The project data key, wrapped by the registrant's master key at key version 1. */
|
|
107
|
+
wrappedProjectKey: Base64,
|
|
108
|
+
});
|
|
109
|
+
export type InitialProjectKey = z.infer<typeof InitialProjectKey>;
|
|
110
|
+
|
|
74
111
|
export const RegisterProjectRequest = z.object({
|
|
75
112
|
projectId: ProjectId,
|
|
76
113
|
/**
|
|
@@ -87,6 +124,8 @@ export const RegisterProjectRequest = z.object({
|
|
|
87
124
|
* caller's personal organization, which every account has.
|
|
88
125
|
*/
|
|
89
126
|
organizationId: z.string().uuid().optional(),
|
|
127
|
+
/** The project's first data key (onboarding B). Honoured only when this call creates the project. */
|
|
128
|
+
initialKey: InitialProjectKey.optional(),
|
|
90
129
|
});
|
|
91
130
|
export type RegisterProjectRequest = z.infer<typeof RegisterProjectRequest>;
|
|
92
131
|
|
|
@@ -103,7 +142,16 @@ export const Project = z.object({
|
|
|
103
142
|
});
|
|
104
143
|
export type Project = z.infer<typeof Project>;
|
|
105
144
|
|
|
106
|
-
export const RegisterProjectResult = z.object({
|
|
145
|
+
export const RegisterProjectResult = z.object({
|
|
146
|
+
project: Project,
|
|
147
|
+
streamId: StreamId,
|
|
148
|
+
/**
|
|
149
|
+
* The key version `initialKey` is stored at for the caller: 1 when this call created the project with it,
|
|
150
|
+
* or repeated the caller's identical version 1; null when no initialKey was sent, or the project already
|
|
151
|
+
* existed with no key (initialKey ignored). Absent from servers older than initialKey.
|
|
152
|
+
*/
|
|
153
|
+
initialKeyVersion: KeyVersion.nullable().optional(),
|
|
154
|
+
});
|
|
107
155
|
export type RegisterProjectResult = z.infer<typeof RegisterProjectResult>;
|
|
108
156
|
|
|
109
157
|
/**
|
|
@@ -162,6 +210,10 @@ export const HomeReplica = z.object({
|
|
|
162
210
|
lastSyncAt: z.iso.datetime().nullable(),
|
|
163
211
|
presence: ReplicaPresence.nullable(),
|
|
164
212
|
presenceAt: z.iso.datetime().nullable(),
|
|
213
|
+
/** When the replica was retired (T123), else null. Absent from servers older than retirement. */
|
|
214
|
+
retiredAt: z.iso.datetime().nullable().optional(),
|
|
215
|
+
/** The retired replica's named successor, else null (not retired, or retired with none). */
|
|
216
|
+
successor: ReplicaId.nullable().optional(),
|
|
165
217
|
});
|
|
166
218
|
export type HomeReplica = z.infer<typeof HomeReplica>;
|
|
167
219
|
|
|
@@ -501,6 +553,15 @@ export type CreateCheckpointResult = z.infer<typeof CreateCheckpointResult>;
|
|
|
501
553
|
export const ListCheckpointsResult = z.object({ checkpoints: z.array(Checkpoint) });
|
|
502
554
|
export type ListCheckpointsResult = z.infer<typeof ListCheckpointsResult>;
|
|
503
555
|
|
|
556
|
+
/**
|
|
557
|
+
* E29 `GET …/checkpoints/head` and E30 `GET …/checkpoints/:checkpointId` (cleo-nexus T122, contract
|
|
558
|
+
* v2.27): one checkpoint, shaped as a list item. Both answer 404 when there is no head, or the id is
|
|
559
|
+
* unknown, malformed or on another stream.
|
|
560
|
+
*/
|
|
561
|
+
export const GetCheckpointResult = z.object({ checkpoint: Checkpoint });
|
|
562
|
+
/** One checkpoint read by id or as the head. */
|
|
563
|
+
export type GetCheckpointResult = z.infer<typeof GetCheckpointResult>;
|
|
564
|
+
|
|
504
565
|
// ---------- blobs ----------
|
|
505
566
|
|
|
506
567
|
export const PresignUploadRequest = z.object({
|
|
@@ -585,6 +646,69 @@ export type ListedLease = z.infer<typeof ListedLease>;
|
|
|
585
646
|
export const ListLeasesResult = z.object({ leases: z.array(ListedLease) });
|
|
586
647
|
export type ListLeasesResult = z.infer<typeof ListLeasesResult>;
|
|
587
648
|
|
|
649
|
+
// ---------- replica retirement (T123; cleocode journal spec §1.3, §3.5 D5) ----------
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* A journal transaction id, `<emittingReplicaId>:<localTxnSeq>`: the replica that emitted the transaction and its own
|
|
653
|
+
* transaction counter. Only the format is checked: the emitting replica may be the successor (a rebind), not the
|
|
654
|
+
* retired one.
|
|
655
|
+
*/
|
|
656
|
+
const JournalTxnId = z
|
|
657
|
+
.string()
|
|
658
|
+
.regex(
|
|
659
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}:(0|[1-9][0-9]{0,15})$/,
|
|
660
|
+
'expected <uuid>:<non-negative integer>',
|
|
661
|
+
);
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* E31 `POST /v1/streams/:streamId/replicas/:replicaId/retirements` (cleo-nexus T123, contract v2.28): the replica
|
|
665
|
+
* writes no segment past `lastReplicaSeq` (none at all when it is null). The journal's own `retire` transaction
|
|
666
|
+
* travels E2E-encrypted; this is the server's signed copy, so it can refuse later appends and show the retirement in
|
|
667
|
+
* the reads.
|
|
668
|
+
*/
|
|
669
|
+
export const RetireReplicaRequest = z.object({
|
|
670
|
+
/** The signing device. It must be the calling device. */
|
|
671
|
+
deviceId: DeviceId,
|
|
672
|
+
/** The replica that carries on this one's history, or null. Never the retired replica itself. */
|
|
673
|
+
successor: ReplicaId.nullable(),
|
|
674
|
+
/**
|
|
675
|
+
* The replica's last segment: at least its highest appended replicaSeq, and late segments up to it still land. Null
|
|
676
|
+
* when the replica wrote nothing: then no segment of it is stored, and none may land.
|
|
677
|
+
*/
|
|
678
|
+
lastReplicaSeq: z.number().int().nonnegative().nullable(),
|
|
679
|
+
/**
|
|
680
|
+
* The journal transaction that carries the retire (`${emittingReplicaId}:${localTxnSeq}`), or null for an owner's
|
|
681
|
+
* retirement with no journal transaction.
|
|
682
|
+
*/
|
|
683
|
+
txnId: JournalTxnId.nullable(),
|
|
684
|
+
/** Ed25519 by the calling device over replicaRetireMessage(...). */
|
|
685
|
+
signature: Base64,
|
|
686
|
+
});
|
|
687
|
+
/** The E31 request body. */
|
|
688
|
+
export type RetireReplicaRequest = z.infer<typeof RetireReplicaRequest>;
|
|
689
|
+
|
|
690
|
+
/** A stored retirement: one per (stream, replica), insert-only. */
|
|
691
|
+
export const ReplicaRetirement = z.object({
|
|
692
|
+
streamId: StreamId,
|
|
693
|
+
replicaId: ReplicaId,
|
|
694
|
+
successor: ReplicaId.nullable(),
|
|
695
|
+
/** Null: the replica wrote nothing. */
|
|
696
|
+
lastReplicaSeq: z.number().int().nonnegative().nullable(),
|
|
697
|
+
/** The device that signed it: the replica's own, a project owner's, or (home streams) another of the account's. */
|
|
698
|
+
signerDeviceId: DeviceId,
|
|
699
|
+
txnId: JournalTxnId.nullable(),
|
|
700
|
+
/** The signer's Ed25519 over replicaRetireMessage(...), so a client or an auditor can verify the record. */
|
|
701
|
+
signature: Base64,
|
|
702
|
+
retiredAt: z.iso.datetime(),
|
|
703
|
+
});
|
|
704
|
+
/** A stored replica retirement. */
|
|
705
|
+
export type ReplicaRetirement = z.infer<typeof ReplicaRetirement>;
|
|
706
|
+
|
|
707
|
+
/** 201 for a new retirement, 200 with the stored one for a repeat with the same successor and lastReplicaSeq. */
|
|
708
|
+
export const RetireReplicaResult = z.object({ retirement: ReplicaRetirement });
|
|
709
|
+
/** The E31 answer. */
|
|
710
|
+
export type RetireReplicaResult = z.infer<typeof RetireReplicaResult>;
|
|
711
|
+
|
|
588
712
|
// ---------- keys (wrapped project keys; the account master key is escrowed on the server, T082: encryption at rest, not end-to-end) ----------
|
|
589
713
|
|
|
590
714
|
/**
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* @since SG-ARCH-SOLID Saga T9831 · E-CLI-BOUNDARY T9833 · T10061
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { DOCS_LIFECYCLE_STATUSES } from '../operations/docs.js';
|
|
12
|
+
import { DOCS_LIFECYCLE_STATUSES } from '../operations/docs-lifecycle.js';
|
|
13
13
|
import { OUTPUT_CONTRACTS } from '../operations/output-contracts-data.js';
|
|
14
14
|
import { tasksUpdateInputContract } from '../operations/tasks.js';
|
|
15
15
|
import type { CanonicalDomain, Gateway, OperationDef, ParamDef, Tier } from './operation-def.js';
|
package/src/doctor.ts
CHANGED
|
@@ -1124,8 +1124,71 @@ export interface SupersededStoreConflict {
|
|
|
1124
1124
|
targetTable: string;
|
|
1125
1125
|
/** Legacy rows left uncopied. */
|
|
1126
1126
|
rows: number;
|
|
1127
|
-
/**
|
|
1128
|
-
|
|
1127
|
+
/**
|
|
1128
|
+
* Why they were left. `id-collision-undecided` (full mode, T13172): a live
|
|
1129
|
+
* task holds the legacy task's id and one of the two creation times does
|
|
1130
|
+
* not parse, so the run cannot tell whether they are the same task; it
|
|
1131
|
+
* neither renumbers nor copies the legacy task. `withheld-undecided`: a
|
|
1132
|
+
* task-graph table such a run did not copy at all, so the undecided task's
|
|
1133
|
+
* children, dependencies and criteria cannot attach to the live task.
|
|
1134
|
+
*/
|
|
1135
|
+
reason:
|
|
1136
|
+
| 'live-authoritative'
|
|
1137
|
+
| 'collides-with-live'
|
|
1138
|
+
| 'id-collision-undecided'
|
|
1139
|
+
| 'withheld-undecided';
|
|
1140
|
+
/** The legacy ids left uncopied, when the run can name them. */
|
|
1141
|
+
ids?: string[];
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/**
|
|
1145
|
+
* A legacy task recovered under a fresh id because a DIFFERENT live task holds
|
|
1146
|
+
* its id (a task written into the empty store after a deferred exodus, T13172).
|
|
1147
|
+
* The renumbering happens in a scratch copy of the legacy file, with every
|
|
1148
|
+
* reference to the task re-pointed; the legacy file and live rows are untouched.
|
|
1149
|
+
*
|
|
1150
|
+
* @task T13172
|
|
1151
|
+
*/
|
|
1152
|
+
export interface SupersededStoreIdRemap {
|
|
1153
|
+
/** Logical legacy source. */
|
|
1154
|
+
sourceDb: string;
|
|
1155
|
+
/** The legacy task's id, which the live store uses for another task. */
|
|
1156
|
+
legacyId: string;
|
|
1157
|
+
/** The id the legacy task is recovered under. */
|
|
1158
|
+
newId: string;
|
|
1159
|
+
/** The legacy task's title. */
|
|
1160
|
+
legacyTitle: string;
|
|
1161
|
+
/** Title of the live task that holds `legacyId`. */
|
|
1162
|
+
liveTitle: string;
|
|
1163
|
+
/** `true` when a previous run already recovered it as `newId` (nothing new is copied). */
|
|
1164
|
+
alreadyRecovered: boolean;
|
|
1165
|
+
/** Legacy rows re-pointed from `legacyId` to `newId` (children, dependencies, criteria, …). */
|
|
1166
|
+
referencesRepointed: number;
|
|
1167
|
+
/**
|
|
1168
|
+
* The legacy task's creation time and type, which a later run (and
|
|
1169
|
+
* `cleo show <legacy id>`) checks against the recovered task before trusting
|
|
1170
|
+
* this record (T13183). Absent in receipts written before T13183.
|
|
1171
|
+
*/
|
|
1172
|
+
legacyCreatedAt?: string;
|
|
1173
|
+
legacyType?: string | null;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
/**
|
|
1177
|
+
* One bare legacy table of the live store a reconcile carried: its row count
|
|
1178
|
+
* and key digest when the run verified (T13319). The run records it in the
|
|
1179
|
+
* store (T13320) and in its receipt; the sync refusal for stranded bare rows
|
|
1180
|
+
* stands down for a table that still matches, so a row the runtime later
|
|
1181
|
+
* deletes from the prefixed twin is never mistaken for one never carried.
|
|
1182
|
+
*
|
|
1183
|
+
* @task T13319
|
|
1184
|
+
*/
|
|
1185
|
+
export interface SupersededStoreBareAccount {
|
|
1186
|
+
/** The bare table, e.g. `task_labels`. */
|
|
1187
|
+
table: string;
|
|
1188
|
+
/** Its rows when the run carried it. */
|
|
1189
|
+
rows: number;
|
|
1190
|
+
/** sha256 of its primary-key values in order (every column if keyless). */
|
|
1191
|
+
digest: string;
|
|
1129
1192
|
}
|
|
1130
1193
|
|
|
1131
1194
|
/**
|
|
@@ -1161,8 +1224,17 @@ export interface SupersededStoreReconcileResult {
|
|
|
1161
1224
|
* touch the live task graph, and list everything left in `conflicts`.
|
|
1162
1225
|
*/
|
|
1163
1226
|
mode: 'full' | 'additive';
|
|
1164
|
-
/**
|
|
1227
|
+
/**
|
|
1228
|
+
* Legacy rows deliberately left uncopied: additive mode's live-authoritative
|
|
1229
|
+
* tables, and in full mode the task-id collisions the run cannot decide.
|
|
1230
|
+
*/
|
|
1165
1231
|
conflicts: SupersededStoreConflict[];
|
|
1232
|
+
/**
|
|
1233
|
+
* Legacy tasks whose id a different live task holds, recovered (full mode)
|
|
1234
|
+
* or to be recovered (dry-run) under a new id. Empty in additive mode, which
|
|
1235
|
+
* never writes the task graph.
|
|
1236
|
+
*/
|
|
1237
|
+
remaps: SupersededStoreIdRemap[];
|
|
1166
1238
|
/** `true` when nothing was written. */
|
|
1167
1239
|
dryRun: boolean;
|
|
1168
1240
|
/** Absolute project root. */
|
|
@@ -1185,4 +1257,11 @@ export interface SupersededStoreReconcileResult {
|
|
|
1185
1257
|
receiptPath: string | null;
|
|
1186
1258
|
/** Human-readable explanation, suitable for printing verbatim. */
|
|
1187
1259
|
reason: string;
|
|
1260
|
+
/**
|
|
1261
|
+
* The live store's bare tables this run carried, on a `reconciled` receipt
|
|
1262
|
+
* that copied the bare family (T13319). Audit only: the sync check reads the
|
|
1263
|
+
* record the run wrote into the store itself, which travels with backups
|
|
1264
|
+
* (T13320).
|
|
1265
|
+
*/
|
|
1266
|
+
accounted?: SupersededStoreBareAccount[];
|
|
1188
1267
|
}
|
package/src/index.ts
CHANGED
|
@@ -514,7 +514,9 @@ export type {
|
|
|
514
514
|
SagaAuditResult,
|
|
515
515
|
SagaAuditViolation,
|
|
516
516
|
SagaAuditViolationKind,
|
|
517
|
+
SupersededStoreBareAccount,
|
|
517
518
|
SupersededStoreConflict,
|
|
519
|
+
SupersededStoreIdRemap,
|
|
518
520
|
SupersededStoreReconcileOutcome,
|
|
519
521
|
SupersededStoreReconcileResult,
|
|
520
522
|
SupersededStoreTableCount,
|
|
@@ -2281,6 +2283,7 @@ export type {
|
|
|
2281
2283
|
HeavyHeapSource,
|
|
2282
2284
|
HeavyLeverChange,
|
|
2283
2285
|
HeavyToolResourcePlan,
|
|
2286
|
+
MemoryPressureReading,
|
|
2284
2287
|
ResourceClass,
|
|
2285
2288
|
ResourceDeferral,
|
|
2286
2289
|
ResourceGrant,
|
|
@@ -2320,7 +2323,13 @@ export type {
|
|
|
2320
2323
|
// === Row Identity Types (T12341 — uid merge keys) ===
|
|
2321
2324
|
export type {
|
|
2322
2325
|
RowIdentityKind,
|
|
2326
|
+
RowIdentityNexusAnswer,
|
|
2323
2327
|
RowIdentityRef,
|
|
2328
|
+
RowIdentityRefillCounts,
|
|
2329
|
+
RowIdentityRefillReport,
|
|
2330
|
+
RowIdentityShareSignal,
|
|
2331
|
+
RowIdentityShareSignalCode,
|
|
2332
|
+
RowIdentityShareState,
|
|
2324
2333
|
RowIdentitySpec,
|
|
2325
2334
|
StoredRefUid,
|
|
2326
2335
|
SymmetricEdge,
|
|
@@ -2461,6 +2470,14 @@ export {
|
|
|
2461
2470
|
// Terminal state sets
|
|
2462
2471
|
TERMINAL_TASK_STATUSES,
|
|
2463
2472
|
} from './status-registry.js';
|
|
2473
|
+
// === Store restore (T13240 — restore cleo.db from a named snapshot or backup id) ===
|
|
2474
|
+
export type {
|
|
2475
|
+
StoreRecoverResult,
|
|
2476
|
+
StoreRestoreKept,
|
|
2477
|
+
StoreRestoreResult,
|
|
2478
|
+
StoreRestoreSource,
|
|
2479
|
+
StoreRestoreVerification,
|
|
2480
|
+
} from './store-restore.js';
|
|
2464
2481
|
// === Sub-Accessor Contracts (T9188) ===
|
|
2465
2482
|
export type {
|
|
2466
2483
|
AgentRegistrySubAccessor,
|
|
@@ -3049,12 +3066,21 @@ export type {
|
|
|
3049
3066
|
CloudPaging,
|
|
3050
3067
|
CloudProjectShowResult,
|
|
3051
3068
|
CloudProjectsResult,
|
|
3069
|
+
CloudRetiredReplica,
|
|
3052
3070
|
CloudStatusGlobalStore,
|
|
3071
|
+
CloudStatusHolder,
|
|
3053
3072
|
CloudStatusLocal,
|
|
3054
3073
|
CloudStatusOfflineDetails,
|
|
3055
3074
|
CloudStatusResult,
|
|
3056
3075
|
CloudStatusSummary,
|
|
3076
|
+
CloudStatusSync,
|
|
3077
|
+
CloudStatusSyncStream,
|
|
3057
3078
|
CloudStatusVerdict,
|
|
3079
|
+
CloudSyncFact,
|
|
3080
|
+
CloudSyncFlags,
|
|
3081
|
+
CloudSyncKnown,
|
|
3082
|
+
CloudSyncLag,
|
|
3083
|
+
CloudSyncUnknown,
|
|
3058
3084
|
CloudWarning,
|
|
3059
3085
|
CloudWhoamiResult,
|
|
3060
3086
|
NexusCloudCredential,
|
|
@@ -3078,6 +3104,7 @@ export {
|
|
|
3078
3104
|
NEXUS_DEVICE_LIST_STATES,
|
|
3079
3105
|
NEXUS_DEVICE_STATE_UNKNOWN,
|
|
3080
3106
|
NEXUS_DEVICE_STATES,
|
|
3107
|
+
NEXUS_FEATURE_PROJECT_INITIAL_KEY,
|
|
3081
3108
|
NEXUS_FORBIDDEN_ERRORS,
|
|
3082
3109
|
NEXUS_FORBIDDEN_REASONS,
|
|
3083
3110
|
NEXUS_PAGE_LIMIT_MAX,
|
|
@@ -3120,6 +3147,9 @@ export {
|
|
|
3120
3147
|
export {
|
|
3121
3148
|
type CloudActivityItem,
|
|
3122
3149
|
type CloudActivityResult,
|
|
3150
|
+
type CloudConflict,
|
|
3151
|
+
type CloudConflictResolveResult,
|
|
3152
|
+
type CloudConflictsResult,
|
|
3123
3153
|
type CloudLeaseReleaseResult,
|
|
3124
3154
|
type CloudPushResult,
|
|
3125
3155
|
type CloudRestoreResult,
|
|
@@ -3129,9 +3159,12 @@ export {
|
|
|
3129
3159
|
type CloudVaultStatusResult,
|
|
3130
3160
|
type CloudVaultTable,
|
|
3131
3161
|
type CloudVaultTableDiff,
|
|
3162
|
+
type CloudVerifyDeepCheck,
|
|
3132
3163
|
type CloudVerifyResult,
|
|
3164
|
+
NEXUS_REPLICA_RETIREMENT_REASONS,
|
|
3133
3165
|
NEXUS_VAULT_LEASE_ROLE,
|
|
3134
3166
|
type NexusActivityEvent,
|
|
3167
|
+
type NexusReplicaRetirementReason,
|
|
3135
3168
|
nexusActivityEventSchema,
|
|
3136
3169
|
nexusActivityPageSchema,
|
|
3137
3170
|
nexusLeaseAcquireSchema,
|