@cogenta/fleet 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/dist/agent/assert.d.ts +9 -0
  2. package/dist/agent/assert.d.ts.map +1 -0
  3. package/dist/agent/assert.js +64 -0
  4. package/dist/agent/assert.js.map +1 -0
  5. package/dist/agent/build.d.ts +21 -0
  6. package/dist/agent/build.d.ts.map +1 -0
  7. package/dist/agent/build.js +43 -0
  8. package/dist/agent/build.js.map +1 -0
  9. package/dist/agent/commands.d.ts +39 -0
  10. package/dist/agent/commands.d.ts.map +1 -0
  11. package/dist/agent/commands.js +43 -0
  12. package/dist/agent/commands.js.map +1 -0
  13. package/dist/agent/rollback-handler.d.ts +28 -0
  14. package/dist/agent/rollback-handler.d.ts.map +1 -0
  15. package/dist/agent/rollback-handler.js +52 -0
  16. package/dist/agent/rollback-handler.js.map +1 -0
  17. package/dist/agent/sign.d.ts +18 -0
  18. package/dist/agent/sign.d.ts.map +1 -0
  19. package/dist/agent/sign.js +19 -0
  20. package/dist/agent/sign.js.map +1 -0
  21. package/dist/agent/types.d.ts +86 -0
  22. package/dist/agent/types.d.ts.map +1 -0
  23. package/dist/agent/types.js +2 -0
  24. package/dist/agent/types.js.map +1 -0
  25. package/dist/alerts/alerts.d.ts +42 -0
  26. package/dist/alerts/alerts.d.ts.map +1 -0
  27. package/dist/alerts/alerts.js +98 -0
  28. package/dist/alerts/alerts.js.map +1 -0
  29. package/dist/alerts/conditions.d.ts +25 -0
  30. package/dist/alerts/conditions.d.ts.map +1 -0
  31. package/dist/alerts/conditions.js +28 -0
  32. package/dist/alerts/conditions.js.map +1 -0
  33. package/dist/alerts/tables.d.ts +13 -0
  34. package/dist/alerts/tables.d.ts.map +1 -0
  35. package/dist/alerts/tables.js +27 -0
  36. package/dist/alerts/tables.js.map +1 -0
  37. package/dist/control/commands.d.ts +38 -0
  38. package/dist/control/commands.d.ts.map +1 -0
  39. package/dist/control/commands.js +51 -0
  40. package/dist/control/commands.js.map +1 -0
  41. package/dist/control/identity.d.ts +13 -0
  42. package/dist/control/identity.d.ts.map +1 -0
  43. package/dist/control/identity.js +5 -0
  44. package/dist/control/identity.js.map +1 -0
  45. package/dist/control/ingest.d.ts +46 -0
  46. package/dist/control/ingest.d.ts.map +1 -0
  47. package/dist/control/ingest.js +56 -0
  48. package/dist/control/ingest.js.map +1 -0
  49. package/dist/control/risk.d.ts +66 -0
  50. package/dist/control/risk.d.ts.map +1 -0
  51. package/dist/control/risk.js +130 -0
  52. package/dist/control/risk.js.map +1 -0
  53. package/dist/control/state.d.ts +19 -0
  54. package/dist/control/state.d.ts.map +1 -0
  55. package/dist/control/state.js +67 -0
  56. package/dist/control/state.js.map +1 -0
  57. package/dist/control/tables.d.ts +18 -0
  58. package/dist/control/tables.d.ts.map +1 -0
  59. package/dist/control/tables.js +58 -0
  60. package/dist/control/tables.js.map +1 -0
  61. package/dist/enrollment/store.d.ts +84 -0
  62. package/dist/enrollment/store.d.ts.map +1 -0
  63. package/dist/enrollment/store.js +81 -0
  64. package/dist/enrollment/store.js.map +1 -0
  65. package/dist/enrollment/tables.d.ts +20 -0
  66. package/dist/enrollment/tables.d.ts.map +1 -0
  67. package/dist/enrollment/tables.js +55 -0
  68. package/dist/enrollment/tables.js.map +1 -0
  69. package/dist/enrollment/tokens.d.ts +4 -0
  70. package/dist/enrollment/tokens.d.ts.map +1 -0
  71. package/dist/enrollment/tokens.js +20 -0
  72. package/dist/enrollment/tokens.js.map +1 -0
  73. package/dist/index.d.ts +42 -0
  74. package/dist/index.d.ts.map +1 -0
  75. package/dist/index.js +25 -0
  76. package/dist/index.js.map +1 -0
  77. package/dist/inventory/drift.d.ts +73 -0
  78. package/dist/inventory/drift.d.ts.map +1 -0
  79. package/dist/inventory/drift.js +91 -0
  80. package/dist/inventory/drift.js.map +1 -0
  81. package/dist/reporting/report.d.ts +65 -0
  82. package/dist/reporting/report.d.ts.map +1 -0
  83. package/dist/reporting/report.js +137 -0
  84. package/dist/reporting/report.js.map +1 -0
  85. package/dist/reporting/schedule.d.ts +10 -0
  86. package/dist/reporting/schedule.d.ts.map +1 -0
  87. package/dist/reporting/schedule.js +34 -0
  88. package/dist/reporting/schedule.js.map +1 -0
  89. package/dist/reporting/tables.d.ts +11 -0
  90. package/dist/reporting/tables.d.ts.map +1 -0
  91. package/dist/reporting/tables.js +22 -0
  92. package/dist/reporting/tables.js.map +1 -0
  93. package/dist/rollout/campaign.d.ts +103 -0
  94. package/dist/rollout/campaign.d.ts.map +1 -0
  95. package/dist/rollout/campaign.js +258 -0
  96. package/dist/rollout/campaign.js.map +1 -0
  97. package/dist/rollout/rollback.d.ts +51 -0
  98. package/dist/rollout/rollback.d.ts.map +1 -0
  99. package/dist/rollout/rollback.js +60 -0
  100. package/dist/rollout/rollback.js.map +1 -0
  101. package/dist/rollout/tables.d.ts +19 -0
  102. package/dist/rollout/tables.d.ts.map +1 -0
  103. package/dist/rollout/tables.js +65 -0
  104. package/dist/rollout/tables.js.map +1 -0
  105. package/package.json +45 -0
@@ -0,0 +1,103 @@
1
+ import { type DatabaseHandle } from '@cogenta/core';
2
+ import type { CommandQueueStore } from '../control/commands.js';
3
+ import type { SiteRisk } from '../control/risk.js';
4
+ import type { SiteStateStore } from '../control/state.js';
5
+ import type { InventoryComponentKind } from '../inventory/drift.js';
6
+ /**
7
+ * "## Mises à jour groupées" (`docs/lots/L8-flotte.md`): "1. Sélection d'un
8
+ * site canari. 2. Application, vérification automatique... 3. Si succès,
9
+ * vague suivante — 10%, puis 50%, puis le reste."
10
+ *
11
+ * **The single most important architectural decision in this module**:
12
+ * verification is real but ASYNCHRONOUS. "Le plan de contrôle n'ouvre jamais
13
+ * de connexion [entrante] vers un site" is absolute, since task 1 — a
14
+ * synchronous HTTP probe from the control plane to a site would violate it.
15
+ * "Le site répond, les pages clés se rendent" is instead learned from the
16
+ * site's own NEXT scheduled contact (its regular telemetry push, tasks 2/3's
17
+ * real ingestion path): if a site keeps contacting the control plane after
18
+ * an update was dispatched to it, and its self-reported inventory
19
+ * (`../inventory/drift.js`'s `extractInventory`) now shows the target
20
+ * version, that IS the verification signal — a real property observable
21
+ * without the control plane ever reaching toward the site. No new
22
+ * verification transport is built here; this module only interprets
23
+ * signals tasks 2/3 already produce.
24
+ */
25
+ export type CampaignWaveLabel = 'canary' | '10%' | '50%' | 'rest';
26
+ export interface CampaignWave {
27
+ readonly index: number;
28
+ readonly label: CampaignWaveLabel;
29
+ readonly siteIds: readonly string[];
30
+ }
31
+ export type CampaignStatus = 'in_progress' | 'succeeded' | 'halted';
32
+ export type SiteRolloutStatus = 'dispatched' | 'succeeded' | 'failed';
33
+ export interface SiteRolloutRecord {
34
+ readonly siteId: string;
35
+ readonly waveIndex: number;
36
+ readonly status: SiteRolloutStatus;
37
+ /** The component's version this site reported just before the update was dispatched to it — what a rollback (task 8) restores. `null` when the site had never reported the component at all. */
38
+ readonly preUpdateVersion: string | null;
39
+ readonly dispatchedAt: string;
40
+ readonly resolvedAt: string | null;
41
+ }
42
+ export interface CampaignRecord {
43
+ readonly id: string;
44
+ readonly componentKind: InventoryComponentKind;
45
+ readonly componentName: string;
46
+ readonly targetVersion: string;
47
+ readonly waves: readonly CampaignWave[];
48
+ readonly currentWaveIndex: number;
49
+ readonly status: CampaignStatus;
50
+ readonly haltedReason: string | null;
51
+ readonly createdAt: string;
52
+ /** Sites a failed wave flagged for rollback — real, actionable state task 8's rollback execution consumes. Never populated except by a genuine wave failure. */
53
+ readonly failedSiteIds: readonly string[];
54
+ }
55
+ /**
56
+ * Partitions an already-ordered site-id list into the lot's real four waves.
57
+ * `orderedSiteIds[0]` is the canary by construction — ordering (who goes
58
+ * first) is `orderSitesForCanary`'s job, not this function's; this function
59
+ * only turns "an order" into "wave membership," a real, pure, deterministic
60
+ * split. "10%"/"50%" are read as percentages of the whole fleet (rounded up,
61
+ * minimum one site so a small fleet still gets a real staged rollout rather
62
+ * than one giant wave), each wave's membership disjoint from the ones before
63
+ * it.
64
+ */
65
+ export declare function planWaves(orderedSiteIds: readonly string[]): readonly CampaignWave[];
66
+ /**
67
+ * Real canary-selection rule: the LOWEST-risk site goes first — the safest
68
+ * real choice available, reusing `../control/risk.js`'s already-real
69
+ * scoring rather than an arbitrary pick. A site with no matching risk entry
70
+ * sorts last (never chosen as canary) — an unscored site is not a safe
71
+ * default canary. Ties break on site name for a fully deterministic order.
72
+ */
73
+ export declare function orderSitesForCanary(siteIds: readonly string[], risks: readonly SiteRisk[]): readonly string[];
74
+ export interface StartCampaignInput {
75
+ readonly siteIds: readonly string[];
76
+ readonly risks: readonly SiteRisk[];
77
+ readonly componentKind: InventoryComponentKind;
78
+ readonly componentName: string;
79
+ readonly targetVersion: string;
80
+ }
81
+ export interface RolloutCampaignStore {
82
+ startCampaign(input: StartCampaignInput): Promise<CampaignRecord>;
83
+ /**
84
+ * Re-evaluates the campaign's current wave against real, already-ingested
85
+ * telemetry (never probes a site) — advances to the next wave on full
86
+ * success, halts on any real failure or timeout, or is a no-op if nothing
87
+ * new has happened yet. Idempotent: calling this repeatedly against an
88
+ * unchanged state re-derives the same result rather than double-advancing.
89
+ */
90
+ checkProgress(campaignId: string, now?: () => number, timeoutMs?: number): Promise<CampaignRecord>;
91
+ /** Reloads a campaign from real persisted storage — proves campaign state survives a control-plane restart. */
92
+ getCampaign(campaignId: string): Promise<CampaignRecord | null>;
93
+ /**
94
+ * Every real rollout record for one site within one campaign (task 8's
95
+ * real version-history read path) — ordered oldest wave first. A site
96
+ * only ever appears in one wave per campaign, so this is normally a
97
+ * single-element list, but the shape stays an array rather than assuming
98
+ * that never changes.
99
+ */
100
+ getSiteRolloutRecords(campaignId: string, siteId: string): Promise<readonly SiteRolloutRecord[]>;
101
+ }
102
+ export declare function createRolloutCampaignStore(db: DatabaseHandle, commandQueueStore: CommandQueueStore, stateStore: SiteStateStore, now?: () => number): RolloutCampaignStore;
103
+ //# sourceMappingURL=campaign.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"campaign.d.ts","sourceRoot":"","sources":["../../src/rollout/campaign.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,258 @@
1
+ import { CogentaError, identifier, newId, sql } from '@cogenta/core';
2
+ import { compareVersions, parseVersion } from '@cogenta/plugins';
3
+ import { extractInventory } from '../inventory/drift.js';
4
+ import { ROLLOUT_TABLES } from './tables.js';
5
+ /**
6
+ * "Un échec arrête toute la campagne" — waves 10%/50%/rest of a campaign
7
+ * halted after wave N never receive an `update` command at all (not "receive
8
+ * one but are told to ignore it"): `startCampaign`/`checkProgress` below only
9
+ * ever call `commandQueueStore.enqueue` for the wave they are actively
10
+ * dispatching, and a halted campaign's `checkProgress` is a real no-op.
11
+ */
12
+ const DEFAULT_TIMEOUT_MS = 24 * 60 * 60 * 1000; // one real daily contact cycle, matching the retention/cadence reasoning in `../control/state.ts`
13
+ /**
14
+ * Partitions an already-ordered site-id list into the lot's real four waves.
15
+ * `orderedSiteIds[0]` is the canary by construction — ordering (who goes
16
+ * first) is `orderSitesForCanary`'s job, not this function's; this function
17
+ * only turns "an order" into "wave membership," a real, pure, deterministic
18
+ * split. "10%"/"50%" are read as percentages of the whole fleet (rounded up,
19
+ * minimum one site so a small fleet still gets a real staged rollout rather
20
+ * than one giant wave), each wave's membership disjoint from the ones before
21
+ * it.
22
+ */
23
+ export function planWaves(orderedSiteIds) {
24
+ const total = orderedSiteIds.length;
25
+ if (total === 0)
26
+ return [];
27
+ const waves = [];
28
+ let cursor = 0;
29
+ const canary = orderedSiteIds.slice(cursor, cursor + 1);
30
+ cursor += canary.length;
31
+ waves.push({ index: 0, label: 'canary', siteIds: canary });
32
+ if (cursor >= total)
33
+ return waves;
34
+ const tenPercentCount = Math.max(1, Math.ceil(total * 0.1));
35
+ const wave2End = Math.min(total, cursor + tenPercentCount);
36
+ const wave2 = orderedSiteIds.slice(cursor, wave2End);
37
+ cursor = wave2End;
38
+ if (wave2.length > 0)
39
+ waves.push({ index: waves.length, label: '10%', siteIds: wave2 });
40
+ if (cursor >= total)
41
+ return waves;
42
+ const fiftyPercentCount = Math.max(1, Math.ceil(total * 0.5));
43
+ const wave3End = Math.min(total, cursor + fiftyPercentCount);
44
+ const wave3 = orderedSiteIds.slice(cursor, wave3End);
45
+ cursor = wave3End;
46
+ if (wave3.length > 0)
47
+ waves.push({ index: waves.length, label: '50%', siteIds: wave3 });
48
+ if (cursor >= total)
49
+ return waves;
50
+ const rest = orderedSiteIds.slice(cursor);
51
+ if (rest.length > 0)
52
+ waves.push({ index: waves.length, label: 'rest', siteIds: rest });
53
+ return waves;
54
+ }
55
+ /**
56
+ * Real canary-selection rule: the LOWEST-risk site goes first — the safest
57
+ * real choice available, reusing `../control/risk.js`'s already-real
58
+ * scoring rather than an arbitrary pick. A site with no matching risk entry
59
+ * sorts last (never chosen as canary) — an unscored site is not a safe
60
+ * default canary. Ties break on site name for a fully deterministic order.
61
+ */
62
+ export function orderSitesForCanary(siteIds, risks) {
63
+ const byId = new Map(risks.map((risk) => [risk.siteId, risk]));
64
+ return [...siteIds].sort((a, b) => {
65
+ const riskA = byId.get(a);
66
+ const riskB = byId.get(b);
67
+ const scoreA = riskA?.score ?? Number.POSITIVE_INFINITY;
68
+ const scoreB = riskB?.score ?? Number.POSITIVE_INFINITY;
69
+ if (scoreA !== scoreB)
70
+ return scoreA - scoreB;
71
+ const nameA = riskA?.siteName ?? a;
72
+ const nameB = riskB?.siteName ?? b;
73
+ return nameA.localeCompare(nameB);
74
+ });
75
+ }
76
+ function campaignNotFound(campaignId) {
77
+ throw new CogentaError({
78
+ code: 'FLEET_CAMPAIGN_NOT_FOUND',
79
+ message: `No rollout campaign with id "${campaignId}".`,
80
+ hint: 'Check the campaign id, or call startCampaign first.',
81
+ details: { campaignId },
82
+ });
83
+ }
84
+ /** An internal invariant violation — a campaign row this store itself just wrote is unreadable a moment later. Never expected; a real, typed error rather than a bare throw if it somehow happens. */
85
+ function campaignStateCorrupt(campaignId, when) {
86
+ throw new CogentaError({
87
+ code: 'FLEET_CAMPAIGN_STATE_CORRUPT',
88
+ message: `Campaign "${campaignId}" could not be reloaded ${when} — its own row is missing.`,
89
+ hint: 'This indicates a storage-layer bug, not a caller error.',
90
+ details: { campaignId, when },
91
+ });
92
+ }
93
+ function versionReached(reported, target) {
94
+ if (reported === target)
95
+ return true;
96
+ const reportedParsed = parseVersion(reported);
97
+ const targetParsed = parseVersion(target);
98
+ if (reportedParsed === null || targetParsed === null)
99
+ return false;
100
+ return compareVersions(reportedParsed, targetParsed) >= 0;
101
+ }
102
+ export function createRolloutCampaignStore(db, commandQueueStore, stateStore, now = Date.now) {
103
+ const campaigns = identifier(ROLLOUT_TABLES.campaigns, db.dialect);
104
+ const siteStatus = identifier(ROLLOUT_TABLES.siteStatus, db.dialect);
105
+ async function componentVersionOf(siteId, kind, name) {
106
+ const snapshot = await stateStore.latest(siteId);
107
+ if (snapshot === null)
108
+ return null;
109
+ const inventory = extractInventory(snapshot);
110
+ const component = inventory.components.find((c) => c.kind === kind && c.name === name);
111
+ return component?.version ?? null;
112
+ }
113
+ async function dispatchWave(campaignId, wave, componentKind, componentName, targetVersion) {
114
+ const dispatchedAt = new Date(now()).toISOString();
115
+ for (const siteId of wave.siteIds) {
116
+ const preUpdateVersion = await componentVersionOf(siteId, componentKind, componentName);
117
+ await commandQueueStore.enqueue(siteId, 'update', {
118
+ componentKind,
119
+ componentName,
120
+ targetVersion,
121
+ });
122
+ await db.query(sql `
123
+ insert into ${siteStatus}
124
+ (id, campaign_id, site_id, wave_index, status, pre_update_version, dispatched_at, resolved_at)
125
+ values
126
+ (${newId(now)}, ${campaignId}, ${siteId}, ${wave.index}, ${'dispatched'}, ${preUpdateVersion}, ${dispatchedAt}, ${null})`);
127
+ }
128
+ }
129
+ async function loadCampaign(campaignId) {
130
+ const result = await db.query(sql `select id, component_kind, component_name, target_version, waves_json, current_wave_index, status, halted_reason, created_at from ${campaigns} where id = ${campaignId}`);
131
+ const row = result.rows[0];
132
+ if (row === undefined)
133
+ return null;
134
+ const failedRows = await db.query(sql `select distinct site_id from ${siteStatus} where campaign_id = ${campaignId} and status = ${'failed'}`);
135
+ return {
136
+ id: row.id,
137
+ componentKind: row.component_kind,
138
+ componentName: row.component_name,
139
+ targetVersion: row.target_version,
140
+ waves: JSON.parse(row.waves_json),
141
+ currentWaveIndex: Number(row.current_wave_index),
142
+ status: row.status,
143
+ haltedReason: row.halted_reason,
144
+ createdAt: row.created_at,
145
+ failedSiteIds: failedRows.rows.map((r) => r.site_id),
146
+ };
147
+ }
148
+ return {
149
+ async startCampaign(input) {
150
+ const orderedIds = orderSitesForCanary(input.siteIds, input.risks);
151
+ const waves = planWaves(orderedIds);
152
+ const id = newId(now);
153
+ const createdAt = new Date(now()).toISOString();
154
+ await db.query(sql `
155
+ insert into ${campaigns}
156
+ (id, component_kind, component_name, target_version, waves_json, current_wave_index, status, halted_reason, created_at)
157
+ values
158
+ (${id}, ${input.componentKind}, ${input.componentName}, ${input.targetVersion}, ${JSON.stringify(waves)}, ${0}, ${'in_progress'}, ${null}, ${createdAt})`);
159
+ const firstWave = waves[0];
160
+ if (firstWave !== undefined) {
161
+ await dispatchWave(id, firstWave, input.componentKind, input.componentName, input.targetVersion);
162
+ }
163
+ const campaign = await loadCampaign(id);
164
+ if (campaign === null)
165
+ return campaignStateCorrupt(id, 'immediately after insert');
166
+ return campaign;
167
+ },
168
+ async checkProgress(campaignId, clock = now, timeoutMs = DEFAULT_TIMEOUT_MS) {
169
+ const campaign = await loadCampaign(campaignId);
170
+ if (campaign === null)
171
+ return campaignNotFound(campaignId);
172
+ if (campaign.status !== 'in_progress')
173
+ return campaign;
174
+ const currentWave = campaign.waves[campaign.currentWaveIndex];
175
+ if (currentWave === undefined)
176
+ return campaign;
177
+ const rows = await db.query(sql `select id, campaign_id, site_id, wave_index, status, pre_update_version, dispatched_at, resolved_at from ${siteStatus} where campaign_id = ${campaignId} and wave_index = ${currentWave.index}`);
178
+ let anyFailed = false;
179
+ let allResolved = true;
180
+ for (const row of rows.rows) {
181
+ if (row.status !== 'dispatched') {
182
+ if (row.status === 'failed')
183
+ anyFailed = true;
184
+ continue;
185
+ }
186
+ const snapshot = await stateStore.latest(row.site_id);
187
+ const dispatchedAt = row.dispatched_at === null ? 0 : new Date(row.dispatched_at).getTime();
188
+ if (snapshot !== null && new Date(snapshot.ingestedAt).getTime() > dispatchedAt) {
189
+ const inventory = extractInventory(snapshot);
190
+ const component = inventory.components.find((c) => c.kind === campaign.componentKind && c.name === campaign.componentName);
191
+ const reached = component !== undefined && versionReached(component.version, campaign.targetVersion);
192
+ const resolvedAt = new Date(clock()).toISOString();
193
+ const status = reached ? 'succeeded' : 'failed';
194
+ await db.query(sql `update ${siteStatus} set status = ${status}, resolved_at = ${resolvedAt} where id = ${row.id}`);
195
+ if (!reached)
196
+ anyFailed = true;
197
+ continue;
198
+ }
199
+ // No new, post-dispatch telemetry yet — inconclusive. A real,
200
+ // bounded timeout also counts as failure: a site that never checks
201
+ // back in is exactly as unsafe to advance past as one that reports
202
+ // a real problem.
203
+ if (clock() - dispatchedAt > timeoutMs) {
204
+ const resolvedAt = new Date(clock()).toISOString();
205
+ await db.query(sql `update ${siteStatus} set status = ${'failed'}, resolved_at = ${resolvedAt} where id = ${row.id}`);
206
+ anyFailed = true;
207
+ continue;
208
+ }
209
+ allResolved = false;
210
+ }
211
+ if (anyFailed) {
212
+ await db.query(sql `
213
+ update ${campaigns} set status = ${'halted'}, halted_reason = ${`wave "${currentWave.label}" reported a failure`}
214
+ where id = ${campaignId}`);
215
+ const halted = await loadCampaign(campaignId);
216
+ if (halted === null)
217
+ return campaignStateCorrupt(campaignId, 'during halt');
218
+ return halted;
219
+ }
220
+ if (!allResolved) {
221
+ const stillInProgress = await loadCampaign(campaignId);
222
+ if (stillInProgress === null)
223
+ return campaignStateCorrupt(campaignId, 'mid-check');
224
+ return stillInProgress;
225
+ }
226
+ // Every site in the current wave succeeded — advance, or finish.
227
+ const nextIndex = campaign.currentWaveIndex + 1;
228
+ const nextWave = campaign.waves[nextIndex];
229
+ if (nextWave === undefined) {
230
+ await db.query(sql `update ${campaigns} set status = ${'succeeded'} where id = ${campaignId}`);
231
+ }
232
+ else {
233
+ await db.query(sql `update ${campaigns} set current_wave_index = ${nextIndex} where id = ${campaignId}`);
234
+ await dispatchWave(campaignId, nextWave, campaign.componentKind, campaign.componentName, campaign.targetVersion);
235
+ }
236
+ const advanced = await loadCampaign(campaignId);
237
+ if (advanced === null)
238
+ return campaignStateCorrupt(campaignId, 'after advancing');
239
+ return advanced;
240
+ },
241
+ getCampaign: loadCampaign,
242
+ async getSiteRolloutRecords(campaignId, siteId) {
243
+ const rows = await db.query(sql `
244
+ select id, campaign_id, site_id, wave_index, status, pre_update_version, dispatched_at, resolved_at
245
+ from ${siteStatus} where campaign_id = ${campaignId} and site_id = ${siteId}
246
+ order by wave_index asc`);
247
+ return rows.rows.map((row) => ({
248
+ siteId: row.site_id,
249
+ waveIndex: row.wave_index,
250
+ status: row.status,
251
+ preUpdateVersion: row.pre_update_version,
252
+ dispatchedAt: row.dispatched_at ?? '',
253
+ resolvedAt: row.resolved_at,
254
+ }));
255
+ },
256
+ };
257
+ }
258
+ //# sourceMappingURL=campaign.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"campaign.js","sourceRoot":"","sources":["../../src/rollout/campaign.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ import type { CommandQueueStore, FleetCommand } from '../control/commands.js';
2
+ import type { InventoryComponentKind } from '../inventory/drift.js';
3
+ import type { CampaignRecord, RolloutCampaignStore } from './campaign.js';
4
+ /**
5
+ * "## Mises à jour groupées" (`docs/lots/L8-flotte.md`): "Un échec arrête
6
+ * toute la campagne et **propose** le retour arrière du site concerné."
7
+ *
8
+ * "Propose", not "déclenche" — a halted campaign never auto-enqueues a
9
+ * rollback by itself. An unattended automatic rollback of what might be a
10
+ * transient contact failure (task 7's `checkProgress` treats "no telemetry
11
+ * within the timeout" the same as a real reported failure, and a real
12
+ * network hiccup is a legitimate transient cause) is a real operational risk
13
+ * the lot's own words don't ask for. `listRollbackCandidates` below is the
14
+ * "propose" half — real, queryable data, never an action by itself.
15
+ * `triggerRollback` is the real, separate act a human (or, later, some
16
+ * explicit automation a deployer opts into) performs deliberately.
17
+ */
18
+ export interface RollbackCandidate {
19
+ readonly siteId: string;
20
+ readonly componentKind: InventoryComponentKind;
21
+ readonly componentName: string;
22
+ /** What this site reported for the component just before the campaign updated it — `null` when the site had never reported it at all, in which case there is nothing real to roll back to. */
23
+ readonly rollbackToVersion: string | null;
24
+ }
25
+ /**
26
+ * Real rollback candidates for a halted campaign — one entry per site the
27
+ * campaign's own real wave-verification flagged as failed
28
+ * (`CampaignRecord.failedSiteIds`, task 7), each carrying the real version
29
+ * that same site reported immediately before this campaign touched it
30
+ * (`SiteRolloutRecord.preUpdateVersion`, task 7's own per-site history — not
31
+ * `../control/state.js`'s general drift-tracking history, which has no
32
+ * concept of "before THIS campaign"). Returns an empty list for a campaign
33
+ * that is not halted — there is nothing to propose while it is still
34
+ * running or after it fully succeeded.
35
+ */
36
+ export declare function listRollbackCandidates(campaignStore: RolloutCampaignStore, campaign: CampaignRecord): Promise<readonly RollbackCandidate[]>;
37
+ /**
38
+ * Enqueues a real, signed `rollback` command (task 6's real command-queue
39
+ * primitive) for exactly one site — strictly per-site, the same isolation
40
+ * discipline every prior L8 task has held: there is no "roll back the whole
41
+ * fleet" operation, matching "## Mises à jour groupées"'s own words
42
+ * verbatim: "Chaque site conserve son propre historique de version et peut
43
+ * être revenu en arrière indépendamment. Il n'existe pas d'état global à
44
+ * restaurer." Callable standalone, independent of any campaign — an
45
+ * operator deciding a specific site needs to go back does not need a
46
+ * campaign to exist at all; `listRollbackCandidates` above is one real way
47
+ * to arrive at the right `(siteId, componentKind, componentName, version)`
48
+ * tuple, not the only way to call this.
49
+ */
50
+ export declare function triggerRollback(commandQueueStore: CommandQueueStore, siteId: string, componentKind: InventoryComponentKind, componentName: string, rollbackToVersion: string | null): Promise<FleetCommand>;
51
+ //# sourceMappingURL=rollback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rollback.d.ts","sourceRoot":"","sources":["../../src/rollout/rollback.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAA;AAC7E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AACnE,OAAO,KAAK,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAA;AAEzE;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,aAAa,EAAE,sBAAsB,CAAA;IAC9C,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,8LAA8L;IAC9L,QAAQ,CAAC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1C;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,sBAAsB,CAC1C,aAAa,EAAE,oBAAoB,EACnC,QAAQ,EAAE,cAAc,GACvB,OAAO,CAAC,SAAS,iBAAiB,EAAE,CAAC,CAkBvC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,eAAe,CACnC,iBAAiB,EAAE,iBAAiB,EACpC,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,sBAAsB,EACrC,aAAa,EAAE,MAAM,EACrB,iBAAiB,EAAE,MAAM,GAAG,IAAI,GAC/B,OAAO,CAAC,YAAY,CAAC,CAcvB"}
@@ -0,0 +1,60 @@
1
+ import { CogentaError } from '@cogenta/core';
2
+ /**
3
+ * Real rollback candidates for a halted campaign — one entry per site the
4
+ * campaign's own real wave-verification flagged as failed
5
+ * (`CampaignRecord.failedSiteIds`, task 7), each carrying the real version
6
+ * that same site reported immediately before this campaign touched it
7
+ * (`SiteRolloutRecord.preUpdateVersion`, task 7's own per-site history — not
8
+ * `../control/state.js`'s general drift-tracking history, which has no
9
+ * concept of "before THIS campaign"). Returns an empty list for a campaign
10
+ * that is not halted — there is nothing to propose while it is still
11
+ * running or after it fully succeeded.
12
+ */
13
+ export async function listRollbackCandidates(campaignStore, campaign) {
14
+ if (campaign.status !== 'halted')
15
+ return [];
16
+ const candidates = [];
17
+ for (const siteId of campaign.failedSiteIds) {
18
+ const records = await campaignStore.getSiteRolloutRecords(campaign.id, siteId);
19
+ // A site appears at most once per campaign in practice (task 7's wave
20
+ // partition is disjoint) — the failed/most recent record is the real
21
+ // one to read "what it reported right before this campaign" from.
22
+ const record = records.at(-1);
23
+ candidates.push({
24
+ siteId,
25
+ componentKind: campaign.componentKind,
26
+ componentName: campaign.componentName,
27
+ rollbackToVersion: record?.preUpdateVersion ?? null,
28
+ });
29
+ }
30
+ return candidates;
31
+ }
32
+ /**
33
+ * Enqueues a real, signed `rollback` command (task 6's real command-queue
34
+ * primitive) for exactly one site — strictly per-site, the same isolation
35
+ * discipline every prior L8 task has held: there is no "roll back the whole
36
+ * fleet" operation, matching "## Mises à jour groupées"'s own words
37
+ * verbatim: "Chaque site conserve son propre historique de version et peut
38
+ * être revenu en arrière indépendamment. Il n'existe pas d'état global à
39
+ * restaurer." Callable standalone, independent of any campaign — an
40
+ * operator deciding a specific site needs to go back does not need a
41
+ * campaign to exist at all; `listRollbackCandidates` above is one real way
42
+ * to arrive at the right `(siteId, componentKind, componentName, version)`
43
+ * tuple, not the only way to call this.
44
+ */
45
+ export async function triggerRollback(commandQueueStore, siteId, componentKind, componentName, rollbackToVersion) {
46
+ if (rollbackToVersion === null) {
47
+ throw new CogentaError({
48
+ code: 'FLEET_ROLLBACK_NO_PRIOR_VERSION',
49
+ message: `No prior version is known for "${componentName}" on site "${siteId}" — nothing to roll back to.`,
50
+ hint: 'A rollback needs a real, previously-reported version. Check the site actually reported this component before the change that needs reverting.',
51
+ details: { siteId, componentKind, componentName },
52
+ });
53
+ }
54
+ return await commandQueueStore.enqueue(siteId, 'rollback', {
55
+ componentKind,
56
+ componentName,
57
+ targetVersion: rollbackToVersion,
58
+ });
59
+ }
60
+ //# sourceMappingURL=rollback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rollback.js","sourceRoot":"","sources":["../../src/rollout/rollback.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AA2B5C;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,aAAmC,EACnC,QAAwB;IAExB,IAAI,QAAQ,CAAC,MAAM,KAAK,QAAQ;QAAE,OAAO,EAAE,CAAA;IAE3C,MAAM,UAAU,GAAwB,EAAE,CAAA;IAC1C,KAAK,MAAM,MAAM,IAAI,QAAQ,CAAC,aAAa,EAAE,CAAC;QAC5C,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,qBAAqB,CAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC,CAAA;QAC9E,sEAAsE;QACtE,qEAAqE;QACrE,kEAAkE;QAClE,MAAM,MAAM,GAAG,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;QAC7B,UAAU,CAAC,IAAI,CAAC;YACd,MAAM;YACN,aAAa,EAAE,QAAQ,CAAC,aAAa;YACrC,aAAa,EAAE,QAAQ,CAAC,aAAa;YACrC,iBAAiB,EAAE,MAAM,EAAE,gBAAgB,IAAI,IAAI;SACpD,CAAC,CAAA;IACJ,CAAC;IACD,OAAO,UAAU,CAAA;AACnB,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,iBAAoC,EACpC,MAAc,EACd,aAAqC,EACrC,aAAqB,EACrB,iBAAgC;IAEhC,IAAI,iBAAiB,KAAK,IAAI,EAAE,CAAC;QAC/B,MAAM,IAAI,YAAY,CAAC;YACrB,IAAI,EAAE,iCAAiC;YACvC,OAAO,EAAE,kCAAkC,aAAa,cAAc,MAAM,8BAA8B;YAC1G,IAAI,EAAE,+IAA+I;YACrJ,OAAO,EAAE,EAAE,MAAM,EAAE,aAAa,EAAE,aAAa,EAAE;SAClD,CAAC,CAAA;IACJ,CAAC;IACD,OAAO,MAAM,iBAAiB,CAAC,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE;QACzD,aAAa;QACb,aAAa;QACb,aAAa,EAAE,iBAAiB;KACjC,CAAC,CAAA;AACJ,CAAC"}
@@ -0,0 +1,19 @@
1
+ import { type DatabaseHandle } from '@cogenta/core';
2
+ export declare const ROLLOUT_TABLES: {
3
+ readonly campaigns: 'cogenta_fleet_rollout_campaigns';
4
+ readonly siteStatus: 'cogenta_fleet_rollout_site_status';
5
+ };
6
+ /**
7
+ * Same `create table if not exists` pattern as `../control/tables.ts` — no
8
+ * separate migration file. `campaigns` is one row per rollout campaign, its
9
+ * whole wave plan persisted as JSON so a campaign genuinely survives a
10
+ * control-plane restart ("le plan de contrôle indisponible n'affecte aucun
11
+ * site" cuts both ways: an in-flight campaign's own state must be real,
12
+ * durable data, never in-memory only). `site_status` is one row per
13
+ * (campaign, site) — the real per-site version-history record a rollback
14
+ * (task 8) needs: `pre_update_version` is what to roll back TO, not a
15
+ * duplicate of `../control/state.ts`'s general telemetry history, which
16
+ * tracks drift over time rather than "what this one campaign changed."
17
+ */
18
+ export declare function ensureRolloutTables(db: DatabaseHandle): Promise<void>;
19
+ //# sourceMappingURL=tables.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tables.d.ts","sourceRoot":"","sources":["../../src/rollout/tables.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EAKpB,MAAM,eAAe,CAAA;AAEtB,eAAO,MAAM,cAAc;aACzB,SAAS,EAAE,iCAAiC;aAC5C,UAAU,EAAE,mCAAmC;CACvC,CAAA;AA0BV;;;;;;;;;;;GAWG;AACH,wBAAsB,mBAAmB,CAAC,EAAE,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAsC3E"}
@@ -0,0 +1,65 @@
1
+ import { identifier, sql, unsafeRaw, } from '@cogenta/core';
2
+ export const ROLLOUT_TABLES = {
3
+ campaigns: 'cogenta_fleet_rollout_campaigns',
4
+ siteStatus: 'cogenta_fleet_rollout_site_status',
5
+ };
6
+ function textColumn(dialect, length) {
7
+ return unsafeRaw(dialect === 'sqlite' ? 'text' : `varchar(${length})`);
8
+ }
9
+ function longTextColumn(dialect) {
10
+ return unsafeRaw(dialect === 'sqlite' ? 'text' : 'text');
11
+ }
12
+ /** A real integer column — `current_wave_index`/`wave_index` are ordinal positions compared and incremented as numbers, never text. */
13
+ function intColumn() {
14
+ return unsafeRaw('integer');
15
+ }
16
+ async function createIndexIfMissing(db, name, table, columns) {
17
+ await db
18
+ .query(sql `create index ${identifier(name, db.dialect)} on ${table} ${columns}`)
19
+ .catch(() => undefined); // already there — no portable "if not exists" for indexes
20
+ }
21
+ /**
22
+ * Same `create table if not exists` pattern as `../control/tables.ts` — no
23
+ * separate migration file. `campaigns` is one row per rollout campaign, its
24
+ * whole wave plan persisted as JSON so a campaign genuinely survives a
25
+ * control-plane restart ("le plan de contrôle indisponible n'affecte aucun
26
+ * site" cuts both ways: an in-flight campaign's own state must be real,
27
+ * durable data, never in-memory only). `site_status` is one row per
28
+ * (campaign, site) — the real per-site version-history record a rollback
29
+ * (task 8) needs: `pre_update_version` is what to roll back TO, not a
30
+ * duplicate of `../control/state.ts`'s general telemetry history, which
31
+ * tracks drift over time rather than "what this one campaign changed."
32
+ */
33
+ export async function ensureRolloutTables(db) {
34
+ const d = db.dialect;
35
+ const t255 = textColumn(d, 255);
36
+ const longText = longTextColumn(d);
37
+ const int = intColumn();
38
+ const campaigns = identifier(ROLLOUT_TABLES.campaigns, d);
39
+ await db.query(sql `
40
+ create table if not exists ${campaigns} (
41
+ id ${t255} not null primary key,
42
+ component_kind ${t255} not null,
43
+ component_name ${t255} not null,
44
+ target_version ${t255} not null,
45
+ waves_json ${longText} not null,
46
+ current_wave_index ${int} not null,
47
+ status ${t255} not null,
48
+ halted_reason ${longText},
49
+ created_at ${t255} not null
50
+ )`);
51
+ const siteStatus = identifier(ROLLOUT_TABLES.siteStatus, d);
52
+ await db.query(sql `
53
+ create table if not exists ${siteStatus} (
54
+ id ${t255} not null primary key,
55
+ campaign_id ${t255} not null,
56
+ site_id ${t255} not null,
57
+ wave_index ${int} not null,
58
+ status ${t255} not null,
59
+ pre_update_version ${t255},
60
+ dispatched_at ${t255},
61
+ resolved_at ${t255}
62
+ )`);
63
+ await createIndexIfMissing(db, 'cogenta_fleet_rollout_site_status_campaign', siteStatus, sql `(campaign_id, site_id)`);
64
+ }
65
+ //# sourceMappingURL=tables.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tables.js","sourceRoot":"","sources":["../../src/rollout/tables.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,UAAU,EAEV,GAAG,EACH,SAAS,GACV,MAAM,eAAe,CAAA;AAEtB,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,SAAS,EAAE,iCAAiC;IAC5C,UAAU,EAAE,mCAAmC;CACvC,CAAA;AAEV,SAAS,UAAU,CAAC,OAAwB,EAAE,MAAc;IAC1D,OAAO,SAAS,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,MAAM,GAAG,CAAC,CAAA;AACxE,CAAC;AAED,SAAS,cAAc,CAAC,OAAwB;IAC9C,OAAO,SAAS,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAA;AAC1D,CAAC;AAED,uIAAuI;AACvI,SAAS,SAAS;IAChB,OAAO,SAAS,CAAC,SAAS,CAAC,CAAA;AAC7B,CAAC;AAED,KAAK,UAAU,oBAAoB,CACjC,EAAkB,EAClB,IAAY,EACZ,KAAkB,EAClB,OAAoB;IAEpB,MAAM,EAAE;SACL,KAAK,CAAC,GAAG,CAAA,gBAAgB,UAAU,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,KAAK,IAAI,OAAO,EAAE,CAAC;SAC/E,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA,CAAC,0DAA0D;AACtF,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,EAAkB;IAC1D,MAAM,CAAC,GAAG,EAAE,CAAC,OAAO,CAAA;IACpB,MAAM,IAAI,GAAG,UAAU,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;IAC/B,MAAM,QAAQ,GAAG,cAAc,CAAC,CAAC,CAAC,CAAA;IAClC,MAAM,GAAG,GAAG,SAAS,EAAE,CAAA;IAEvB,MAAM,SAAS,GAAG,UAAU,CAAC,cAAc,CAAC,SAAS,EAAE,CAAC,CAAC,CAAA;IACzD,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;iCACa,SAAS;WAC/B,IAAI;uBACQ,IAAI;uBACJ,IAAI;uBACJ,IAAI;mBACR,QAAQ;2BACA,GAAG;eACf,IAAI;sBACG,QAAQ;mBACX,IAAI;MACjB,CAAC,CAAA;IAEL,MAAM,UAAU,GAAG,UAAU,CAAC,cAAc,CAAC,UAAU,EAAE,CAAC,CAAC,CAAA;IAC3D,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;iCACa,UAAU;WAChC,IAAI;oBACK,IAAI;gBACR,IAAI;mBACD,GAAG;eACP,IAAI;2BACQ,IAAI;sBACT,IAAI;oBACN,IAAI;MAClB,CAAC,CAAA;IACL,MAAM,oBAAoB,CACxB,EAAE,EACF,4CAA4C,EAC5C,UAAU,EACV,GAAG,CAAA,wBAAwB,CAC5B,CAAA;AACH,CAAC"}