Skip to content

feat(checkpoint): câblage réel du checkpoint projet coordonné (CTR-CHECKPOINT débloqué) - #32

Open
openaxcloud wants to merge 7 commits into
mainfrom
feat/project-checkpoint-wiring
Open

feat(checkpoint): câblage réel du checkpoint projet coordonné (CTR-CHECKPOINT débloqué)#32
openaxcloud wants to merge 7 commits into
mainfrom
feat/project-checkpoint-wiring

Conversation

@openaxcloud

Copy link
Copy Markdown
Owner

Quoi (déblocage d'un des 4 contrats bloqués — refus : « tests unitaires mais aucun câblage réel »)

La machine lifecycle-state-machines.ts (0 appelant hors specs) est maintenant CÂBLÉE derrière de vrais endpoints :

  • Machine étendue (plan §15) : PREPARING→QUIESCING→BARRIER_ESTABLISHED→VOLUME_SNAPSHOTTING→DB_SNAPSHOTTING→(POD optionnel)→VERIFYING→COMMITTED — garde « pas de snapshot avant barrière » conservée ; projectCheckpointAdmissible : un snapshot de pod seul n'est jamais un checkpoint projet.
  • Barrière d'écriture RÉELLE : pendant le quiesce, les écritures fichiers (zip import + runtime write) reçoivent 423 CHECKPOINT_BARRIER_ACTIVE ; dégel garanti (finally + auto-expiration 30 s) — prouvé sur le chemin d'échec par injection de panne.
  • Manifeste complet : logicalBarrierId, consistencyLevel (application/crash-consistent calculé), components + hashes, restoreCompatibility, dependenciesDeclared, expiresAt.
  • Restore VÉRIFIÉ : POST /checkpoints/:id/restore-verify rejoue le snapshot dans un projet jetable et compare le hash au manifeste — jamais d'écrasement du source ; prouvé : une modification post-checkpoint n'apparaît pas dans le restore.
  • DB honnête : backup physique CNPG (takeSnapshot) câblé quand DB_ROLLBACK_ENABLED + base provisionnée ; sinon dépendance déclarée dans le manifeste — dit tel quel, conformément à la directive.
  • Prisma ProjectCheckpoint (migration 0079 — 0078 réservé au ledger PR feat(billing): grand livre canonique à double entrée durable (C1 / P0-V3-12) #28).

Preuves

  • 18 tests : machine 13 + 5 routes (barrière observée EN VOL avec snapshot retenu, 423 puis dégel ; panne injectée → CLEANED + barrière levée ; restore vérifié par hash dans un projet jetable ; pod-seul inadmissible ; DB-sans-composant refusée sauf déclaration).
  • Suite api complète : 1195 verts (1 timeout flake env re-passé isolément). Typecheck + build stricts : 0 erreur.
  • CHECKPOINT_CONTRACT.md v2 (structure complète ; dépendance CNPG dormante + preuve PITR live déclarées ouvertes).

⚠️ Pas de merge sans feu vert d'Avi. CONTRACT_REGISTRY (branche PR #29) sera aligné à la réconciliation.

🤖 Generated with Claude Code

…15, CTR-CHECKPOINT)

- Machine étendue : VOLUME_SNAPSHOTTING → DB_SNAPSHOTTING → POD (optionnel) ;
  garde snapshot-avant-barrière conservée ; projectCheckpointAdmissible :
  un snapshot de pod seul n'est JAMAIS un checkpoint projet.
- Endpoints réels : POST /projects/:id/checkpoints (orchestrateur complet),
  GET, POST /restore-verify (restore dans un projet JETABLE + hash comparé).
- Barrière d'écriture RÉELLE : 423 CHECKPOINT_BARRIER_ACTIVE sur les écritures
  fichiers pendant le quiesce ; dégel GARANTI (finally + auto-expiration).
- Manifeste complet : logicalBarrierId, consistencyLevel, components+hashes,
  restoreCompatibility, dependenciesDeclared, expiresAt (TTL 30 j).
- DB : backup physique CNPG câblé si DB_ROLLBACK_ENABLED + base provisionnée ;
  sinon dépendance DÉCLARÉE (jamais un silence).
- Prisma ProjectCheckpoint (migration 0079 — 0078 réservé au ledger PR #28).
- 18 tests (barrière observée EN VOL, dégel sur panne injectée, restore
  vérifié, pod-seul refusé) ; suite api 1195 verts ; typecheck+build stricts 0.
- CHECKPOINT_CONTRACT v2 (structure complète, dépendance CNPG dormante déclarée).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 21, 2026 04:23
@github-actions

Copy link
Copy Markdown
Contributor

ℹ️ Preview deployment not configured

Name Info
Latest commit 3f75fa0
Status Preview deployment requires Cloudflare secrets

To enable preview deployments, repository maintainers can add:

  • CLOUDFLARE_API_TOKEN secret
  • CLOUDFLARE_ACCOUNT_ID secret

Built with E-Code

openaxcloud pushed a commit that referenced this pull request Jul 21, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
openaxcloud pushed a commit that referenced this pull request Jul 21, 2026
…tre↔fichier satisfaite

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0fb937448d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread services/api/src/app.ts
* MÊME barrière logique, manifeste visible seulement une fois tout vérifié.
* Un snapshot de pod seul n'est JAMAIS un checkpoint projet.
*/
const checkpointBarriers = new Map<string, { barrierId: string; expiresAt: number }>();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Share the checkpoint barrier across API replicas

In the inspected production Helm configuration, infra/helm/platform/values-prod.yaml runs two API replicas and permits scaling to six, but this barrier exists only in one process. A checkpoint handled by replica A therefore does not block a file mutation routed to replica B, so production checkpoints can be committed while writes are still occurring; store the barrier in shared/transactional state or otherwise route and coordinate all project mutations consistently.

Useful? React with 👍 / 👎.

Comment thread services/api/src/app.ts
Comment on lines +15102 to +15104
// Barrière de checkpoint (plan §15) : écritures gelées pendant le quiesce.
if (authorized.projectId && rejectIfCheckpointBarrier(reply, authorized.projectId)) {
return reply;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Gate every runtime file mutation on the barrier

Even on a single API replica, this check protects only the PUT write operation. The adjacent create-file, create-directory, delete, and move handlers at lines 15116-15150 still mutate the same workspace while a checkpoint is in flight, allowing the captured tree to change during snapshotting; apply the barrier to every file mutation path rather than only /files/write and ZIP import.

Useful? React with 👍 / 👎.

Comment thread services/api/src/app.ts
Comment on lines +21518 to +21519
} else if (c.componentKind === 'DATABASE') {
c.verified = true; // le CR de backup a été appliqué ; la preuve PITR complète = restore réel

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Verify CNPG backup completion before committing

For a provisioned database with rollback enabled, takeSnapshot only applies the CNPG Backup resource and returns before that backup succeeds, and it can also return applied: false when the environment resolves to NoopProvisioner. This unconditional assignment nevertheless marks either result verified, allowing checkpointManifestVisible to commit a checkpoint whose database backup is pending, failed, or was never submitted; verification must inspect terminal backup status and reject inactive provisioning.

Useful? React with 👍 / 👎.

Comment thread services/api/src/app.ts
Comment on lines +21388 to +21390
if (Date.now() > b.expiresAt) {
checkpointBarriers.delete(projectId); // dégel par timeout — jamais un gel permanent
return undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Abort checkpoints when the barrier expires

When archiving a large project or initiating its database snapshot takes longer than 30 seconds, a subsequent mutation deletes this barrier and proceeds, but the checkpoint workflow itself keeps running and may still transition to COMMITTED. That permits changes between component snapshots despite the manifest claiming one logical barrier; expiration must abort/fail the checkpoint rather than merely thaw writes while it continues.

Useful? React with 👍 / 👎.

Comment thread services/api/src/app.ts
Comment on lines +21456 to +21459
const fileSnapshot = await store.createSnapshot({
projectId,
kind: 'manual',
manifest: { checkpoint: true, files: files.map((f) => f.path), logicalBarrierId: barrierId, contentHash: filesHash },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Enforce snapshot quotas for checkpoint archives

Each checkpoint creates a user-visible manual project snapshot and archive here without the snapshots.count and snapshots.sizeMb checks used by the normal snapshot routes, and it records no size usage. A user with project write access can therefore invoke this endpoint repeatedly after reaching their plan limits and consume unbounded shared storage; serialize the quota checks and snapshot insertion and record the corresponding usage.

Useful? React with 👍 / 👎.

Comment thread services/api/src/app.ts
Comment on lines +21441 to +21443
// BARRIÈRE RÉELLE : les endpoints d'écriture fichiers renvoient 423 tant
// qu'elle tient ; auto-expiration = dégel garanti même si ce processus meurt.
checkpointBarriers.set(projectId, { barrierId, expiresAt: Date.now() + CHECKPOINT_QUIESCE_TIMEOUT_MS });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Serialize concurrent checkpoints per project

If two checkpoint requests for the same project overlap, the second call overwrites the first barrier entry, and whichever call finishes first unconditionally deletes the other call's barrier in finally. The remaining checkpoint then continues without write protection; reject an already-active checkpoint or serialize runs per project and only remove the barrier when its ID matches.

Useful? React with 👍 / 👎.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Cette PR implémente le câblage réel du “checkpoint projet coordonné” côté API, en branchant la machine lifecycle-state-machines.ts derrière des endpoints concrets (création, lecture, restore-verify) avec une barrière d’écriture pendant la phase de quiesce, et en persistants les checkpoints via Prisma.

Changes:

  • Ajout d’un modèle Prisma ProjectCheckpoint + migration 0079, et implémentation store (Prisma + test store).
  • Ajout des routes POST /projects/:projectId/checkpoints, GET /projects/:projectId/checkpoints/:checkpointId et POST /.../restore-verify, avec orchestration de la machine et génération de manifeste.
  • Ajout d’une suite de tests d’intégration des routes + mise à jour des tests de la machine/guards.

Reviewed changes

Copilot reviewed 10 out of 16 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
services/api/src/app.ts Ajoute la barrière + orchestration checkpoint, endpoints create/get/restore-verify, et blocage 423 sur certaines écritures pendant la barrière.
services/api/src/lifecycle-state-machines.ts Étend la machine checkpoint (VOLUME/DB/POD), renforce la garde “pas de snapshot avant barrière”, ajoute projectCheckpointAdmissible.
services/api/src/lifecycle-state-machines.spec.ts Met à jour les tests de transitions et les helpers de snapshot component (ajout componentKind).
services/api/src/store.ts Étend ApiStore avec CRUD minimal pour ProjectCheckpoint.
services/api/src/prisma-store.ts Implémente create/update/getProjectCheckpoint via Prisma.
services/api/src/tests/test-api-store.ts Ajoute un stockage in-memory pour ProjectCheckpoint dans les tests.
services/api/src/tests/checkpoint-routes.spec.ts Nouveaux tests E2E routes + barrière 423 + restore-verify hash dans un projet jetable.
packages/database/prisma/schema.prisma Ajoute le modèle ProjectCheckpoint.
packages/database/prisma/migrations/0079_project_checkpoint/migration.sql Migration additive: création table + index.
packages/database/generated/client/* Mise à jour du Prisma client généré (nouveau model + enums).
docs/parity/CHECKPOINT_CONTRACT.md Mise à jour du contrat/checkpoint v2 + ancrage d’implémentation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread services/api/src/app.ts
Comment on lines +21379 to +21394
const checkpointBarriers = new Map<string, { barrierId: string; expiresAt: number }>();

const activeCheckpointBarrier = (projectId: string) => {
const b = checkpointBarriers.get(projectId);

if (!b) {
return undefined;
}

if (Date.now() > b.expiresAt) {
checkpointBarriers.delete(projectId); // dégel par timeout — jamais un gel permanent
return undefined;
}

return b;
};
Comment thread services/api/src/app.ts
Comment on lines +21388 to +21390
if (Date.now() > b.expiresAt) {
checkpointBarriers.delete(projectId); // dégel par timeout — jamais un gel permanent
return undefined;
Comment thread services/api/src/app.ts
const ckpt = await store.createProjectCheckpoint({ projectId, createdByUserId: request.currentUser?.id });

let state: CheckpointState = 'PREPARING';
const advance = async (to: CheckpointState, patch: Record<string, unknown> = {}) => {
Comment thread services/api/src/app.ts
Comment on lines +21451 to +21463
const files = await listProjectFilesIncludingIdeState(store, projectStorage, projectId);
const archive = await projectStorage.createSnapshot({ projectId, label: `checkpoint ${barrierId}`, files });
const filesHash = createHash('sha256')
.update(files.map((f) => `${f.path}\n${f.content}`).join('\x00'))
.digest('hex');
const fileSnapshot = await store.createSnapshot({
projectId,
kind: 'manual',
manifest: { checkpoint: true, files: files.map((f) => f.path), logicalBarrierId: barrierId, contentHash: filesHash },
storageKey: archive.storageKey,
byteLength: archive.byteLength,
createdByUserId: request.currentUser?.id,
});
Comment thread services/api/src/app.ts
Comment on lines +21510 to +21513
const reread = await projectStorage.getSnapshotFiles(archive.storageKey);
const rereadHash = createHash('sha256')
.update(reread.map((f) => `${f.path}\n${f.content}`).join('\x00'))
.digest('hex');
Comment thread services/api/src/app.ts
Comment on lines +21542 to +21554
const manifest = {
logicalBarrierId: barrierId,
consistencyLevel: components.every((c) => c.consistencyLevel === 'application-consistent')
? 'application-consistent'
: 'crash-consistent',
components,
contentHashes: { files: filesHash },
restoreCompatibility: { files: 'project-files-v1', database: databaseProvisioned ? 'cnpg-pitr-v1' : 'n/a' },
dependenciesDeclared: databaseDependencyDeclared
? ['DATABASE : base provisionnée mais snapshot CNPG dormant (DB_ROLLBACK_ENABLED off) — checkpoint fichiers-seuls, dit tel quel']
: [],
expiresAt: new Date(Date.now() + CHECKPOINT_TTL_DAYS * 86_400_000).toISOString(),
};
Comment thread services/api/src/app.ts
Comment on lines +21581 to +21586
app.post('/projects/:projectId/checkpoints', async (request, reply) => {
const project = await requireProject(request, store, parse(projectParams, request.params).projectId, 'projects:write');
await requireOrg(request, store, project.organizationId, 'projects:write');

const result = await runProjectCheckpoint({ request, projectId: project.id });

Comment thread services/api/src/app.ts
Comment on lines +21639 to +21642
const files = await getSnapshotFiles(snapshot);
const restoredHash = createHash('sha256')
.update(files.map((f) => `${f.path}\n${f.content}`).join('\x00'))
.digest('hex');
Comment on lines +161 to +170
// Laisser l'orchestrateur atteindre la barrière puis tenter une écriture.
await new Promise((r) => setTimeout(r, 50));
const during = await app.inject({
method: 'POST',
url: `/projects/${project.id}/files/import/zip`,
headers: auth('ckpt-token'),
payload: { zipBase64: 'UEsFBgAAAAAAAAAAAAAAAAAAAAAAAA==' }, // zip vide valide
});
expect(during.statusCode).toBe(423);
expect(during.json().code).toBe('CHECKPOINT_BARRIER_ACTIVE');
openaxcloud added a commit that referenced this pull request Jul 21, 2026
…cis v2 (#29)

* docs(parity): chantier C5 vague 1 — 14 contrats individualisés, 3 durcis v2 (billing ledger, import/remix, gallery)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(parity): C5 vague 2 — 7 contrats durcis v2 (10/14), schéma manifeste durci pour de vrai

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(parity): CTR-CHECKPOINT → durci v2 (câblé PR #32) — 11/14

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(parity): CHECKPOINT_CONTRACT v2 (identique PR #32) — garde registre↔fichier satisfaite

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Avi <avi@snatchbot.me>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

ℹ️ Preview deployment not configured

Name Info
Latest commit 27e8252
Status Preview deployment requires Cloudflare secrets

To enable preview deployments, repository maintainers can add:

  • CLOUDFLARE_API_TOKEN secret
  • CLOUDFLARE_ACCOUNT_ID secret

Built with E-Code

Avi and others added 3 commits July 21, 2026 15:12
…sections + garde no-mocks sur commentaire values-prod)

Mêmes fixes que sur la branche docs/verdict-04-corrections (PR #40) : la
section admin agent-routing (fee92bd, 16/07) n'avait pas bumpé le test, et
la garde no-mocks (fake) bloquait sur un commentaire descriptif D2.
Débloque Production CI / Quality Gates de cette PR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nce croisée au drill DR joué (PR #36, 13 min 06 s) — CNPG projet reste ouvert, dit tel quel

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…0078)

Schéma auto-mergé proprement (les 2 blocs conservés) ; client REGÉNÉRÉ, jamais
mergé à la main. Rejoué après merge : suite api complète 1261 verts / 0 échec.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

ℹ️ Preview deployment not configured

Name Info
Latest commit f89ef2d
Status Preview deployment requires Cloudflare secrets

To enable preview deployments, repository maintainers can add:

  • CLOUDFLARE_API_TOKEN secret
  • CLOUDFLARE_ACCOUNT_ID secret

Built with E-Code

…rat checkpoint (drift-check)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

ℹ️ Preview deployment not configured

Name Info
Latest commit 0f34023
Status Preview deployment requires Cloudflare secrets

To enable preview deployments, repository maintainers can add:

  • CLOUDFLARE_API_TOKEN secret
  • CLOUDFLARE_ACCOUNT_ID secret

Built with E-Code

… flake « Timeout calling onTaskUpdate »

execFileSync bloquait l'event-loop du worker ~60 s pendant la collecte
« vitest list » en CI chargée : le worker ne répondait plus au RPC du pool et
vitest sortait en erreur non gérée avec toute la suite verte (reproduit 2/2
sur cette PR, 1× sur la #40). Passage en execFile promisifié + timeout 180 s.
Rejoué local : spec vert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

ℹ️ Preview deployment not configured

Name Info
Latest commit 41373d0
Status Preview deployment requires Cloudflare secrets

To enable preview deployments, repository maintainers can add:

  • CLOUDFLARE_API_TOKEN secret
  • CLOUDFLARE_ACCOUNT_ID secret

Built with E-Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants