diff --git a/AGENTS.md b/AGENTS.md index 00c59fe..a8a0fff 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,7 +34,9 @@ through an explicitly authorized, explicitly scoped human ticket: - F0 — PASS/FROZEN - Phase H v1 — PASS/FROZEN - MVS-M1 — PASS/FROZEN -- Project DB v21 — PASS/FROZEN +- Project DB v22 — PASS/FROZEN +- Calibration Bootstrap v1 — PASS/FROZEN +- Selected Scientific Execution — PASS/FROZEN - Photo Quality Triage / Acquisition Selection — PASS/FROZEN - S1 Capture / Asset Provenance — PASS/FROZEN - S2 Capture-safe Standard Ingestion — PASS/FROZEN diff --git a/README.md b/README.md index 2039a20..aa0ac7f 100644 --- a/README.md +++ b/README.md @@ -34,10 +34,14 @@ persistante, enrichissable et versionnable. - **Image View** : vues triées et filtrées pour la TUI - **Task** : moteur de tâches avec pause/reprise, annulation et séquences - **Task Checkpoint v1** : snapshot durable, fichier atomique et reprise sûre -- **Project Database v21** : fondations v20 préservées, avec persistance additive - des résultats de triage photo +- **Project Database v22** : fondations v20/v21 préservées, avec snapshot + d'exécution sélectionnée, publication bornée des représentations, attache de + scope de calibration et tâche durable mince `raw.develop` — PASS / FROZEN - **[Photo Quality Triage](docs/architecture/photo_quality_triage.md)** : métriques JPEG - bornées et recommandations non destructives, avec persistance additive v21 +- **[Calibration Bootstrap v1](docs/architecture/calibration_bootstrap.md)** : + import borné d'une calibration optimisée et traçable avant le Sparse SfM à + calibration connue — PASS / FROZEN ; ni + auto-calibration interne ni EXIF comme source de calibration scientifique - **Exécution durable de campagne d'acquisition** : tâche générique à requête typée immuable, confirmations `CALLER_EXPLICIT`, curseur et correspondance tâche/groupe→Capture persistants ; un groupe S3-E par séquence, reprise par @@ -72,7 +76,11 @@ persistante, enrichissable et versionnable. réels A6000 (953 paires confirmées `CALLER_EXPLICIT`) et Samsung SM-G990B (3544 JPEG singleton) ont été validés dans deux ScanSets d'un même projet temporaire, avec exécution durable, Governor/Queue et reprise sans Capture - dupliqué. La suite reste le pipeline scientifique aval, selon la + dupliqué. Les campagnes réelles actuellement évaluées sont + `CALIBRATION_UNAVAILABLE`, donc le Sparse SfM réel est + `BLOCKED_BY_KNOWN_CALIBRATION_DATA` : ce n'est ni un échec logiciel, ni un + rejet de qualité, ni une autorisation d'importer une pseudo-calibration. La + suite reste le pipeline scientifique aval, selon la [roadmap canonique](docs/roadmap/roadmap.md). ### Plus tard / différé @@ -210,6 +218,15 @@ les paramètres dans `L3DMDID2` v2 (220 octets). Chaque appel utilise un espace travail privé neuf sous le staging appelant, sans réemploi d'une scène, profondeur, cache ou sortie antérieure. Le DAG, le viewer et les autres étapes denses restent des tickets séparés planifiés. +Project DB v22, `raw.develop` et Calibration Bootstrap v1 sont **PASS / +FROZEN** : la suite normale 53/53, les contrôles syntaxiques C17, `git diff +--check`, la validation ciblée ASan/UBSan et l'audit final ont passé. La suite +ASan/UBSan complète demeure qualifiée par le comportement LSan du pilote tiers +RADV ; elle n'est pas présentée comme un PASS complet du dépôt. Les campagnes +réelles S21 et A6000 Engine Bay sont +`CALIBRATION_UNAVAILABLE` par non-identifiabilité scientifique des données de +calibration connues ; le Sparse SfM réel reste +`BLOCKED_BY_KNOWN_CALIBRATION_DATA`, sans pseudo-calibration ni import inféré. Le Resource Governor ne constitue pas un Resource System générique : voir la décision d’architecture. diff --git a/docs/architecture/calibration_bootstrap.md b/docs/architecture/calibration_bootstrap.md new file mode 100644 index 0000000..75ae47d --- /dev/null +++ b/docs/architecture/calibration_bootstrap.md @@ -0,0 +1,148 @@ +# Calibration Bootstrap v1 + +**PASS / FROZEN.** Project DB v22 implements the +immutable selected-execution snapshot, explicit cross-task mappings, representation cursor and +calibration-scope attachment described below. The bounded artifact validator/importer produces explicit +immutable `IMPORTED_TRUSTED` calibrations for the existing known-calibration +Sparse SfM contract. It is not self-calibration inside Sparse SfM and never +makes EXIF a scientific calibration source. Solver execution remains outside this importer and is not +claimed as implemented by this stage. + +## Boundary and identity + +Sparse SfM continues to consume an explicit Track Set and immutable calibration +scope. Bootstrap artifacts can support a `Lardon3DSparseCalibration`; external +poses, tracks, matches and points are diagnostics only and never become Lardon3D +scientific reconstruction results. Each artifact identifies a calibration +evidence run, not a Capture, Asset, `image_id`, path, basename, Task ID or +external reconstruction. It records ordered representation hashes/dimensions, +solver executable identity/model/configuration, initialization evidence, +optimized parameters and validation diagnostics. Its SHA-256 is the immutable +`IMPORTED_TRUSTED` provenance fingerprint. + +## Artifact format v1 + +The import API borrows at most 600000 artifact bytes and requires their SHA-256 to equal an +independently supplied expected digest before parsing. The format is fixed-width little-endian; binary64 +uses IEEE-754 bits, rejects non-finite values and canonicalizes negative zero before calibration +publication. Native structs, padding, locale text and trailing bytes are never accepted. + +```text +magic 8 bytes = ASCII "L3DCALB1" +format_version u32 = 1 +model_kind u32 = PINHOLE(1) +model_version u32 = 1 +entry_count u32, 1..4096 and equal to selected item_count +solver_executable_sha256 32 bytes, nonzero +solver_configuration_sha256 32 bytes, nonzero +initialization_evidence_sha256 32 bytes, nonzero +validation_evidence_sha256 32 bytes, nonzero +for each selected item in immutable item_index order: + image_id u64 + representation_sha256 32 bytes + width, height u32, u32 + fx, fy, cx, cy f64, f64, f64, f64 + k1, k2, p1, p2 f64, f64, f64, f64 + support_images u32, nonzero + support_observations u32, nonzero + reprojection_rmse_px finite nonnegative f64 + maximum_parameter_delta finite nonnegative f64 + validation_flags u32 = 0x0f +``` + +The four v1 validation bits attest convergence, non-degenerate support, deterministic stability, and +representative-coordinate equivalence to Lardon3D respectively. The importer requires all four; it does +not silently derive missing evidence or invent a calibration threshold. `image_id` and representation +SHA-256 must exactly match the durable selected item and its image asset. Width/height are artifact-owned +geometry evidence and also enter calibration scientific identity. The artifact contains no external pose, +point, track or match field, so those diagnostics cannot cross the import boundary. + +## Exact model requirement + +Bootstrap v1 accepts only an exact equivalent of Lardon3D's OpenCV-style +pinhole model with binary64 `fx, fy, cx, cy, k1, k2, p1, p2`: + +```text +x_d = x * (1 + k1*r2 + k2*r2*r2) + 2*p1*x*y + p2*(r2 + 2*x*x) +y_d = y * (1 + k1*r2 + k2*r2*r2) + p1*(r2 + 2*y*y) + 2*p2*x*y +u = fx*x_d + cx; v = fy*y_d + cy +``` + +The imported geometry is exactly the downstream Feature Store geometry. Models +with active extra coefficients are rejected, never truncated. Representative +coordinates must validate origin, axes, pixel convention and projection against +the Lardon3D implementation before import. + +## Metadata, representations and failure state + +Metadata is initialization/grouping evidence only. Orientation does not identify +a physical camera, though coordinates must match downstream decoding. A6000 +calibrates deterministic RAW-derived PNG geometry, never camera-JPEG geometry +without a separately proven exact transform. Candidate A6000 groups use RAW +geometry, lens evidence and exact EXIF focal only as solver inputs; zoom groups +remain separate unless optimization proves equivalence. S21 uses its selected +source JPEG geometry and solver-supported module grouping. + +Bootstrap requires convergence, finite values, positive focal lengths, valid +principal points, non-degenerate support, model compatibility and deterministic +stability evidence. Unstable groups are expanded or partitioned; metadata +interpolation and guessed calibration are prohibited. An affected selected image +is `CALIBRATION_UNAVAILABLE`, not a quality rejection or corrupt source. + +The real S21 and current A6000 Engine Bay selected campaigns are presently +`CALIBRATION_UNAVAILABLE` because the available evidence is scientifically +non-identifiable for the known-calibration contract. This is neither a source +or quality failure nor a software failure. No pseudo-calibration, metadata +interpolation or inferred import is permitted; real Sparse SfM is consequently +`BLOCKED_BY_KNOWN_CALIBRATION_DATA`. The v22 normal suite passed 53/53, C17 +syntax checks and `git diff --check` passed, targeted ASan/UBSan validation +passed, and final review passed. The full ASan/UBSan suite remains qualified by +third-party RADV LSan behavior and is not represented as a repository-wide clean +sanitizer pass. This bootstrap infrastructure is therefore `PASS / FROZEN`; that +lifecycle state applies to the bounded importer and persistence infrastructure, +not to calibration of either real campaign or to real Sparse SfM execution. + +A dedicated future physical calibration acquisition, with images and evidence +adequate to establish an exact supported model for each relevant representation +group, is recommended before attempting real known-calibration Sparse SfM. +That acquisition is future field work, not an implemented solver, a change to +the scientific contract, or permission to retrofit the present campaigns. + +## Execution relationship and resources + +```text +SCIENTIFIC_EXECUTABLE = QUALITY_SELECTED + intersection REPRESENTATION_READY + intersection CALIBRATION_ASSIGNED +``` + +Quality selection is snapshotted immutably. A durable path must retain explicit +quality-group → campaign-group → Capture mappings: equal numeric group values +from independent Task requests imply no identity. Source photos remain read-only; +RAW bootstrap development processes one image at a time, with no campaign-wide +decoded cache. Ticket-owned artifacts retain source/representation hashes, +output dimensions, policy fingerprint and solver provenance. + +Project DB v22 stores the ordered selected rows and advances representation publication one item +at a time. Each row immutably declares either a Capture-owned SOURCE RAW `asset_id` for S3-B1 or +that the representation is a source image and no RAW identity exists. Creation validates +`capture_source_assets.source_kind` as RAW; later execution never resolves it from path, digest, basename, +request source index or group number. An `image_id` is accepted only through the retained Capture's explicit +`capture_images` relation. The calibration scope is attached only after all representations are +durable and only when that existing immutable scope contains every selected image. These DB +transitions are idempotent exact retries; they do not execute a solver or RAW developer and do not +introduce a scheduler, worker pool, sidecar, or parallel resource owner. + +Artifact validation completes before any publication. Calibration and scope creation then reuse the +existing short Project DB transactions. A crash or final attachment conflict may therefore retain only +immutable content-addressed calibration evidence; it cannot make the selected execution `READY`. An exact +retry reuses that evidence and converges through the existing scope attachment. The importer has no Task, +Queue or Governor ownership and holds only two arrays bounded by the selected item count; solver execution +and one-at-a-time RAW development must be coordinated by the established runtime when invoked. +The thin durable `raw.develop` Task provides that S3-B1 execution boundary for one exact +`capture_id`/`source_asset_id` pair. It reuses the existing single-active Queue and Governor, never +advances the selected-execution cursor itself, and retains its published `image_id` before terminal +generic Task progress can become durable. Its reservation owns one CPU thread, one I/O slot and a +conservative 2 GiB working allowance for the bounded 40 MP decoder and publication buffers only for +the callback lifetime; this operational admission bound is not a scientific Capture-count limit. +A higher-level selected-execution coordinator remains separate scope. diff --git a/docs/architecture/project_database.md b/docs/architecture/project_database.md index dfcecbe..502f58e 100644 --- a/docs/architecture/project_database.md +++ b/docs/architecture/project_database.md @@ -1,5 +1,71 @@ # Base de données projet Lardon3D +## Snapshot d'exécution scientifique sélectionnée — Project DB v22 + +**PASS / FROZEN.** La migration additive v21→v22 ajoute +`capture_source_assets`, `selected_executions`, `selected_execution_items` et +`raw_development_tasks`. +`capture_source_assets` retient l'association explicitement publiée par S3-E entre un Capture, +son asset `SOURCE` et le kind JPEG ou RAW déjà validé. La publication de l'association SOURCE et +du kind est transactionnelle et un retry exact converge ; un autre kind est un conflit. Les +Captures provenant d'une v21 restent honnêtement sans mapping : la migration ne déduit rien de +leur chemin, nom, SHA-256, ordre d'attache ou image logique. + +Un snapshot ordonné retient explicitement +la relation `quality_task_id/group_id → campaign_task_id/group_id → capture_id`; les deux espaces +de `group_id` restent indépendants et une égalité numérique n'est jamais une identité. Les deux +tâches doivent viser le même ScanSet, le triage doit être terminé et chaque résultat retenu doit +être effectivement inclus au moment du snapshot. Le snapshot et son ordre deviennent ensuite +immuables. + +Chaque item reçoit un `image_id` seulement par association explicite déjà durable dans +`capture_images`. La publication de cette représentation et l'avancement du curseur sont +transactionnels ; un retry exact converge, tandis qu'un autre `image_id` est un conflit. Après le +dernier item, un `calibration_scope_id` existant ne peut être attaché que si chaque image retenue +est membre de ce scope. Le stage terminal `READY` représente donc exactement l'intersection +`QUALITY_SELECTED ∩ REPRESENTATION_READY ∩ CALIBRATION_ASSIGNED` sans inférer Capture depuis +Asset, SHA-256, chemin, basename, Task ID, groupe ou `image_id`. + +Le snapshot fixe aussi la source de représentation de chaque item. Un item A6000 retient +explicitement l'`asset_id` RAW déjà associé comme `SOURCE` au Capture ; un item dont la +représentation est une image source retient explicitement l'absence d'identité RAW. Cette paire +discriminant/Asset est immuable et fait partie de l'exact retry du snapshot. La création vérifie +l'association RAW dans `capture_source_assets` ; aucune exécution ultérieure ne retrouve le RAW depuis +un chemin, SHA-256, basename, index source ou numéro de groupe. + +Le curseur est borné à 4096 items et ne réserve aucune ressource d'exécution. Cette fondation v22 +n'ajoute ni scheduler, worker pool, sidecar, cache décodé, ni nouvelle Queue/Governor. Le +coordinateur scientifique et le développement RAW borné restent des étapes d'exécution séparées. +L'importeur Calibration Bootstrap v1 valide un artifact borné contre ce snapshot, crée/réutilise les +calibrations et le scope immuables, puis emploie cette attache v22 ; il n'étend pas le schéma et n'exécute +ni solveur ni développement RAW. + +Le développeur RAW expose aussi une entrée bornée par `source_asset_id` explicite. Elle exige la +relation RAW du Capture, charge le chemin géré depuis cet ID, vérifie les octets contre le SHA-256 +persisté, puis réutilise sans modification RAW Policy v1. Le chemin géré n'est donc qu'un accès aux +octets après résolution d'identité explicite, jamais une méthode de découverte d'identité. + +La v22 amendée ajoute `raw_development_tasks` pour l'exécution durable mince de cette entrée. Une +ligne retient exactement `task_id`, `capture_id`, `source_asset_id`, une phase monotone +`PENDING → PUBLISHED` et, en phase publiée, l'`image_id` dérivée explicitement associée au même +Capture. Le couple Capture/SOURCE RAW est immuable, doit exister dans `capture_source_assets` avec +le kind RAW, et n'est jamais retrouvé depuis un chemin, digest, nom, image ou ID opérationnel. +Snapshot Task générique, référence du checkpoint et ligne typée sont écrits dans une même +transaction. L'asset et l'image immuables peuvent précéder ce checkpoint si le processus meurt : +le retry exact S3-B1 converge par contenu puis publie la phase et l'`image_id` sans deviner +l'identité. Les lectures de reprise rejettent types SQLite, phases, nullabilité et relations +Capture/RAW/image incohérents comme corruption durable. + +La suite normale v22 a passé 53/53, les contrôles syntaxiques C17 et `git diff +--check` ont passé, la validation ciblée ASan/UBSan a passé et l'audit final a +conclu au PASS. La suite ASan/UBSan complète reste qualifiée par le comportement +LSan du pilote tiers RADV ; elle ne constitue pas un PASS complet du dépôt. +Cette évidence gèle la frontière de persistance v22 décrite ici, sans changement +de contrat ni de schéma. Les campagnes réelles peuvent donc conserver un snapshot +et leurs représentations sans être scientifiquement exécutables : une absence de +calibration connue reste `CALIBRATION_UNAVAILABLE`, non une raison d'inférer ou +de créer une identité, une calibration ou une migration supplémentaire. + ## Photo Quality Triage — Project DB v21 **PASS / FROZEN.** La migration additive v20→v21 ajoute diff --git a/docs/architecture/resource_boundary.md b/docs/architecture/resource_boundary.md index 79fe245..abfec5e 100644 --- a/docs/architecture/resource_boundary.md +++ b/docs/architecture/resource_boundary.md @@ -5,7 +5,7 @@ **ACCEPTED** — architecture decision for the post-Gate C documentation freeze. Current Sparse SfM gates A through G are **PASS / FROZEN**. -Project Database: current schema **v20**; historical v16 remains frozen. +Project Database: current schema **v22**; historical v16 remains frozen. This record is normative for the current architecture. It does not introduce an implementation, a public API, a persistence format or a roadmap commitment. diff --git a/docs/roadmap/roadmap.md b/docs/roadmap/roadmap.md index 18d4fdf..92cde9e 100644 --- a/docs/roadmap/roadmap.md +++ b/docs/roadmap/roadmap.md @@ -121,6 +121,26 @@ Queue/Governor, persistance tâche/groupe→Capture et reprise sans duplication. La capacité bornée des propositions de revue conserve un préfixe déterministe; elle ne limite jamais l'évaluation complète ni le groupement scientifique. +### État de calibration des campagnes réelles + +L'infrastructure Project DB v22, exécution sélectionnée, `raw.develop` et +Calibration Bootstrap v1 est **PASS / FROZEN** : la suite normale 53/53, les +contrôles syntaxiques C17, `git diff --check`, la validation ciblée ASan/UBSan +et l'audit final ont passé. La suite ASan/UBSan complète reste qualifiée par le +comportement LSan du pilote tiers RADV ; elle n'est pas déclarée PASS complet du +dépôt. Ce gel concerne l'infrastructure de persistance et d'import borné, pas +la calibration des campagnes réelles ni leur Sparse SfM. + +Les campagnes réelles S21 et A6000 Engine Bay actuellement évaluées sont +`CALIBRATION_UNAVAILABLE` par non-identifiabilité scientifique des données +disponibles pour le contrat Sparse SfM à calibration connue. Ce statut ne +signifie ni échec logiciel, ni défaut des sources, ni rejet de qualité. Sans +pseudo-calibration, interpolation de métadonnées ou import inféré, le Sparse +SfM réel est `BLOCKED_BY_KNOWN_CALIBRATION_DATA`; l'intégration synthétique à +calibration connue a passé. Une acquisition physique dédiée de calibration est +une étape future, décrite dans [Calibration Bootstrap v1](../architecture/calibration_bootstrap.md), +et non une fonctionnalité déjà réalisée. + ## PHOTO QUALITY TRIAGE / ACQUISITION SELECTION — PASS / FROZEN L'étape qualité canonique implémentée se place après la découverte bornée, les @@ -484,8 +504,9 @@ L'ordre demeure sans ambiguïté : ```text CURRENT NEXT - intégration réelle multi-campagne A6000 + S21 - → pipeline scientifique → dense/mesh → publication + acquisition physique dédiée de calibration + → calibration connue validée → Sparse SfM réel multi-campagne + → dense/mesh → publication → LATER Coverage Analysis → Coverage Viewer → suggestions de points de vue → localisation live → intégration HDMI/USB → Capture Guidance / Live Coverage diff --git a/include/lardon3d/calibration_bootstrap.h b/include/lardon3d/calibration_bootstrap.h new file mode 100644 index 0000000..bb1521a --- /dev/null +++ b/include/lardon3d/calibration_bootstrap.h @@ -0,0 +1,59 @@ +#ifndef LARDON3D_CALIBRATION_BOOTSTRAP_H +#define LARDON3D_CALIBRATION_BOOTSTRAP_H + +#include +#include + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +enum { + LARDON3D_CALIBRATION_BOOTSTRAP_ARTIFACT_VERSION = 1, + LARDON3D_CALIBRATION_BOOTSTRAP_MAX_BYTES = 600000, +}; + +typedef enum { + LARDON3D_CALIBRATION_BOOTSTRAP_OK = 0, + LARDON3D_CALIBRATION_BOOTSTRAP_INVALID_ARGUMENT, + LARDON3D_CALIBRATION_BOOTSTRAP_MALFORMED_ARTIFACT, + LARDON3D_CALIBRATION_BOOTSTRAP_PROVENANCE_MISMATCH, + LARDON3D_CALIBRATION_BOOTSTRAP_SELECTION_CONFLICT, + LARDON3D_CALIBRATION_BOOTSTRAP_DB_ERROR, + LARDON3D_CALIBRATION_BOOTSTRAP_OUT_OF_MEMORY, +} Lardon3DCalibrationBootstrapResult; + +typedef struct { + unsigned char artifact_sha256[LARDON3D_PROJECT_DB_SHA256_SIZE]; + Lardon3DSparseCalibrationScope scope; + uint32_t calibration_count; +} Lardon3DCalibrationBootstrapOutput; + +/* Import a complete Calibration Bootstrap v1 artifact for an existing selected + * execution. All pointers are required and artifact bytes are borrowed only + * for the call. The artifact is bounded by MAX_BYTES, uses the documented + * fixed-width little-endian format, and must hash to expected_artifact_sha256. + * Its ordered image IDs and representation SHA-256 values must exactly match + * the execution's durable selected images. Only the exact OpenCV-compatible + * eight-parameter pinhole model is accepted. + * + * Successful import creates/reuses immutable IMPORTED_TRUSTED calibrations, + * creates/reuses their immutable scope, then attaches that scope through the + * selected-execution contract. Exact retries converge. A failure after an + * immutable calibration is created may retain that content-addressed evidence, + * but cannot make the selected execution READY. The function imports no poses, + * points, tracks, matches, EXIF calibration, or Capture identity. */ +Lardon3DCalibrationBootstrapResult lardon3d_calibration_bootstrap_import( + Lardon3DProjectDb *database, uint64_t execution_id, + const unsigned char *artifact, size_t artifact_size, + const unsigned char expected_artifact_sha256[LARDON3D_PROJECT_DB_SHA256_SIZE], + Lardon3DCalibrationBootstrapOutput *output); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/include/lardon3d/project.h b/include/lardon3d/project.h index 3cd6d90..f30ee26 100644 --- a/include/lardon3d/project.h +++ b/include/lardon3d/project.h @@ -67,6 +67,12 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_acquisition_camp Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_photo_quality_task( Lardon3DAppState *state, const Lardon3DTask *task, const Lardon3DProjectDbPhotoQualityTask *parameters); +/* Persist one RAW Task snapshot and its exact Capture/SOURCE RAW phase in the + * same DB transaction. The checkpoint file is atomically published first; OK + * means the file, generic row, checkpoint reference, and typed row are durable. */ +Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_raw_development_task( + Lardon3DAppState *state, const Lardon3DTask *task, + const Lardon3DProjectDbRawDevelopmentTask *parameters); Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_feature_extract_task( Lardon3DAppState *state, const Lardon3DTask *task, const Lardon3DProjectDbFeatureExtractTask *parameters); diff --git a/include/lardon3d/project_db.h b/include/lardon3d/project_db.h index 94d19df..250a2de 100644 --- a/include/lardon3d/project_db.h +++ b/include/lardon3d/project_db.h @@ -11,7 +11,7 @@ #include enum { - LARDON3D_PROJECT_DB_SCHEMA_VERSION = 21, + LARDON3D_PROJECT_DB_SCHEMA_VERSION = 22, LARDON3D_PROJECT_DB_ID_CAPACITY = 65, LARDON3D_PROJECT_DB_KIND_CAPACITY = 65, LARDON3D_PROJECT_DB_PATH_CAPACITY = 4096, @@ -126,6 +126,20 @@ typedef struct { size_t request_size; } Lardon3DProjectDbPhotoQualityTask; +typedef enum { + LARDON3D_RAW_DEVELOPMENT_TASK_PENDING = 1, + LARDON3D_RAW_DEVELOPMENT_TASK_PUBLISHED = 2, +} Lardon3DProjectDbRawDevelopmentTaskPhase; + +typedef struct { + uint64_t task_id; + uint64_t capture_id; + uint64_t source_asset_id; + Lardon3DProjectDbRawDevelopmentTaskPhase phase; + bool has_image; + uint64_t image_id; +} Lardon3DProjectDbRawDevelopmentTask; + typedef struct { uint64_t task_id; uint32_t group_id; @@ -134,6 +148,44 @@ typedef struct { Lardon3DPhotoQualityOverride override_value; } Lardon3DProjectDbPhotoQualityResult; +typedef enum { + LARDON3D_SELECTED_EXECUTION_REPRESENTATIONS = 1, + LARDON3D_SELECTED_EXECUTION_CALIBRATION = 2, + LARDON3D_SELECTED_EXECUTION_READY = 3, +} Lardon3DProjectDbSelectedExecutionStage; + +typedef struct { + uint64_t execution_id; + uint64_t quality_task_id; + uint64_t campaign_task_id; + bool has_calibration_scope; + uint64_t calibration_scope_id; + Lardon3DProjectDbSelectedExecutionStage stage; + uint32_t next_item_index; + uint32_t item_count; + int64_t created_at; +} Lardon3DProjectDbSelectedExecution; + +typedef enum { + /* The selected representation is an existing source image; no RAW + * development identity is present or may be inferred later. */ + LARDON3D_SELECTED_REPRESENTATION_SOURCE_IMAGE = 1, + /* source_asset_id explicitly identifies the Capture-owned SOURCE RAW whose + * deterministic S3-B1 representation is required. */ + LARDON3D_SELECTED_REPRESENTATION_RAW_ASSET = 2, +} Lardon3DProjectDbSelectedRepresentationSource; + +typedef struct { + uint32_t item_index; + uint32_t quality_group_id; + uint32_t campaign_group_id; + uint64_t capture_id; + Lardon3DProjectDbSelectedRepresentationSource representation_source; + uint64_t source_asset_id; + bool has_image; + uint64_t image_id; +} Lardon3DProjectDbSelectedExecutionItem; + typedef struct { uint64_t scanset_id; char name[LARDON3D_PROJECT_DB_SCANSET_NAME_CAPACITY]; @@ -180,6 +232,17 @@ typedef struct { Lardon3DProjectDbCaptureAssetRole role; } Lardon3DProjectDbCaptureAsset; +typedef enum { + LARDON3D_DB_CAPTURE_SOURCE_JPEG = 1, + LARDON3D_DB_CAPTURE_SOURCE_RAW = 2, +} Lardon3DProjectDbCaptureSourceKind; + +typedef struct { + uint64_t capture_id; + uint64_t asset_id; + Lardon3DProjectDbCaptureSourceKind source_kind; +} Lardon3DProjectDbCaptureSourceAsset; + typedef enum { LARDON3D_DB_ASSET_DERIVATION_GENERIC_VERSIONED = 1, } Lardon3DProjectDbAssetDerivationKind; @@ -539,6 +602,11 @@ Lardon3DProjectDbResult lardon3d_project_db_register_image_asset( Lardon3DProjectDb *database, const unsigned char sha256[LARDON3D_PROJECT_DB_SHA256_SIZE], const char *asset_path, uint64_t size_bytes, int64_t created_at, Lardon3DProjectDbImageAsset *asset); +/* Load one immutable managed asset by its explicit durable ID. The caller owns + * output storage. The returned relative path is validated and NUL-terminated; + * this API performs no Capture/source identity discovery. */ +Lardon3DProjectDbResult lardon3d_project_db_load_image_asset( + Lardon3DProjectDb *database, uint64_t asset_id, Lardon3DProjectDbImageAsset *asset); Lardon3DProjectDbResult lardon3d_project_db_publish_derived_capture_image( Lardon3DProjectDb *database, uint64_t capture_id, uint64_t asset_id, const char *original_name, const char *source_path, uint64_t producer_task_id, @@ -574,6 +642,18 @@ Lardon3DProjectDbResult lardon3d_project_db_attach_capture_asset( Lardon3DProjectDbCaptureAssetRole role); Lardon3DProjectDbResult lardon3d_project_db_attach_capture_source_asset( Lardon3DProjectDb *database, uint64_t capture_id, uint64_t asset_id); +/* Atomically attach an existing asset as SOURCE and retain the explicit source + * kind observed by S3-E. Exact retry is idempotent; a different kind conflicts. + * This provenance must never be synthesized from path, name, SHA-256, + * attachment order, or image identity. */ +Lardon3DProjectDbResult lardon3d_project_db_record_capture_source_asset( + Lardon3DProjectDb *database, uint64_t capture_id, uint64_t asset_id, + Lardon3DProjectDbCaptureSourceKind source_kind); +/* Lists only explicitly retained source-kind relations. Captures migrated from + * v21 may truthfully return an empty page; absence is not permission to infer. */ +Lardon3DProjectDbResult lardon3d_project_db_list_capture_source_assets( + Lardon3DProjectDb *database, uint64_t capture_id, uint64_t after_asset_id, + Lardon3DProjectDbCaptureSourceAsset *assets, size_t capacity, size_t *count); Lardon3DProjectDbResult lardon3d_project_db_list_capture_assets( Lardon3DProjectDb *database, uint64_t capture_id, uint64_t after_asset_id, Lardon3DProjectDbCaptureAsset *assets, size_t capacity, size_t *count); @@ -633,6 +713,30 @@ Lardon3DProjectDbResult lardon3d_project_db_record_photo_quality_task( Lardon3DProjectDbResult lardon3d_project_db_load_photo_quality_task( Lardon3DProjectDb *database, uint64_t task_id, unsigned char *request, size_t request_capacity, Lardon3DProjectDbPhotoQualityTask *parameters); +/* Atomically persist generic Task/checkpoint state with one immutable explicit + * Capture/SOURCE RAW identity and its monotonic publication phase. The caller + * must pass `task_kind` == `LARDON3D_RAW_DEVELOPMENT_TASK_KIND` and + * `task_kind_version` == `LARDON3D_RAW_DEVELOPMENT_TASK_KIND_VERSION`. + * Any mismatch is rejected with INVALID_ARGUMENT because generic Task dispatch and + * typed durable payload must remain coherent across restart. PUBLISHED requires + * the exact derived image retained by that Capture; exact retries are idempotent + * and another identity or image conflicts. */ +Lardon3DProjectDbResult lardon3d_project_db_record_raw_development_task( + Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot, + const char *task_kind, uint32_t task_kind_version, + const Lardon3DProjectDbCheckpoint *checkpoint, + const Lardon3DProjectDbRawDevelopmentTask *parameters, int64_t updated_at); +/* Load and validate one durable RAW Task. Persisted durable rows with + * `task_kind`/`task_kind_version` not equal to + * `LARDON3D_RAW_DEVELOPMENT_TASK_KIND`/`LARDON3D_RAW_DEVELOPMENT_TASK_KIND_VERSION` + * are corrupted durable state and return CORRUPT, preserving the contract that + * typed durable payload and generic Task identity never silently diverge. + * Invalid phases, missing explicit RAW provenance, or a published image outside + * the retained Capture are also reported as corrupt durable state; no source + * identity is inferred. */ +Lardon3DProjectDbResult lardon3d_project_db_load_raw_development_task( + Lardon3DProjectDb *database, uint64_t task_id, + Lardon3DProjectDbRawDevelopmentTask *parameters); /* Result publication and next_group_id advance are atomic. result->group_id is * the canonical plan ID in 1..N and must equal the task's current one-based * cursor; next_group_id must equal result->group_id+1 (N+1 after the last @@ -652,6 +756,44 @@ Lardon3DProjectDbResult lardon3d_project_db_load_photo_quality_result( Lardon3DProjectDbResult lardon3d_project_db_set_photo_quality_override( Lardon3DProjectDb *database, uint64_t task_id, uint32_t group_id, Lardon3DPhotoQualityOverride override_value); +/* Create an immutable, ordered QUALITY_SELECTED snapshot. Each item explicitly + * bridges independent quality and campaign group namespaces to a retained + * Capture; equal numeric group values never imply identity. Each item also + * declares either an explicit Capture-owned SOURCE RAW asset or that its + * representation is a source image and therefore has no RAW identity. The DB + * validates RAW asset membership at creation and never recovers it from path, + * SHA-256, basename, source index, or group number. The caller-owned item array + * is borrowed only for the call. Every referenced quality result must be + * effectively included and both tasks must belong to the same ScanSet. An + * exact retry returns the existing execution; a changed mapping conflicts. */ +Lardon3DProjectDbResult lardon3d_project_db_create_selected_execution( + Lardon3DProjectDb *database, uint64_t quality_task_id, + uint64_t campaign_task_id, const Lardon3DProjectDbSelectedExecutionItem *items, + size_t item_count, int64_t created_at, + Lardon3DProjectDbSelectedExecution *execution); +/* Load durable stage/cursor state. Output is caller-owned and contains no + * borrowed storage. Corrupt SQLite types, signs, bounds, or stage/cursor + * relations are rejected before narrowing to the public C17 fields. */ +Lardon3DProjectDbResult lardon3d_project_db_load_selected_execution( + Lardon3DProjectDb *database, uint64_t execution_id, + Lardon3DProjectDbSelectedExecution *execution); +Lardon3DProjectDbResult lardon3d_project_db_load_selected_execution_item( + Lardon3DProjectDb *database, uint64_t execution_id, uint32_t item_index, + Lardon3DProjectDbSelectedExecutionItem *item); +/* Publish one representation mapping and advance the cursor atomically. The + * image must already be explicitly associated with this item's Capture. + * Retrying the exact mapping after cursor advance is idempotent; no SHA, path, + * Asset, basename, or group number is used to recover identity. */ +Lardon3DProjectDbResult lardon3d_project_db_record_selected_representation( + Lardon3DProjectDb *database, uint64_t execution_id, uint32_t item_index, + uint64_t image_id, uint32_t next_item_index); +/* Attach an existing immutable calibration scope only after all selected + * representations are durable and every selected image is a member. The + * successful transition makes the snapshot scientifically executable; an + * exact scope retry is idempotent and a different scope conflicts. */ +Lardon3DProjectDbResult lardon3d_project_db_assign_selected_calibration_scope( + Lardon3DProjectDb *database, uint64_t execution_id, + uint64_t calibration_scope_id); Lardon3DProjectDbResult lardon3d_project_db_allocate_task_id(Lardon3DProjectDb *database, uint64_t *task_id); Lardon3DProjectDbResult lardon3d_project_db_load_task(Lardon3DProjectDb *database, uint64_t task_id, diff --git a/include/lardon3d/raw_development.h b/include/lardon3d/raw_development.h index d52351b..2ebda34 100644 --- a/include/lardon3d/raw_development.h +++ b/include/lardon3d/raw_development.h @@ -50,6 +50,16 @@ Lardon3DRawDevelopmentResult lardon3d_raw_develop_to_capture( uint64_t producer_task_id, int64_t created_at, Lardon3DRawDevelopmentOutput *output ); +/* Develop the immutable managed RAW identified explicitly by source_asset_id. + * The asset must have a durable RAW source-kind relation to capture_id. This + * API validates the managed bytes against their stored SHA-256 and never finds + * source identity from path, name, hash, attachment order, or image identity. + * Output storage is caller-owned; published assets/images remain DB-owned. */ +Lardon3DRawDevelopmentResult lardon3d_raw_develop_asset_to_capture( + Lardon3DAppState *state, uint64_t capture_id, uint64_t source_asset_id, + uint64_t producer_task_id, int64_t created_at, Lardon3DRawDevelopmentOutput *output +); + #ifdef __cplusplus } #endif diff --git a/include/lardon3d/raw_development_task.h b/include/lardon3d/raw_development_task.h new file mode 100644 index 0000000..dffa764 --- /dev/null +++ b/include/lardon3d/raw_development_task.h @@ -0,0 +1,44 @@ +#ifndef LARDON3D_RAW_DEVELOPMENT_TASK_H +#define LARDON3D_RAW_DEVELOPMENT_TASK_H + +#include +#include + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define LARDON3D_RAW_DEVELOPMENT_TASK_KIND "raw.develop" +enum { LARDON3D_RAW_DEVELOPMENT_TASK_KIND_VERSION = 1 }; + +/* Create one durable, initially unqueued S3-B1 Task for the exact existing + * Capture-owned SOURCE RAW asset. The two IDs are immutable and are never + * inferred from paths, hashes, names, images, or Task identity. task_id is + * required and receives the durable operational ID. The caller owns the + * returned Task and must enqueue or destroy it. */ +Lardon3DTask *lardon3d_project_create_raw_development_task( + Lardon3DAppState *state, uint64_t capture_id, uint64_t source_asset_id, + uint64_t *task_id); + +/* Create and transfer one durable RAW Task to state's existing Queue. Queue + * admission owns the Task's bounded CPU/RAM/I/O reservation until callback + * completion, failure, or cancellation; no worker or scheduler is created. */ +bool lardon3d_project_enqueue_raw_development( + Lardon3DAppState *state, uint64_t capture_id, uint64_t source_asset_id, + uint64_t *task_id); + +/* Reconstruct callback state only from the validated typed v22 row. snapshot, + * reconstruction context, and binding are required and caller-owned. The + * binding owns its returned context until the runtime invokes its destroyer. */ +bool lardon3d_raw_development_task_reconstruct( + const Lardon3DTaskDurableSnapshot *snapshot, void *context, + Lardon3DTaskKindBinding *binding); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/meson.build b/meson.build index 38f328c..c998998 100644 --- a/meson.build +++ b/meson.build @@ -149,6 +149,7 @@ executable( 'src/image_catalog.c', 'src/image_catalog_persistent.c', 'src/raw_development.cpp', + 'src/raw_development_task.cpp', 'src/acquisition_pairing.cpp', 'src/acquisition_ingest.cpp', 'src/acquisition_campaign.cpp', @@ -168,6 +169,7 @@ executable( 'src/image_view.c', 'src/project.c', 'src/project_db.c', 'src/project_db_sparse_sfm.c', + 'src/calibration_bootstrap.c', 'src/sparse_sfm_geometry.cpp', 'src/sparse_sfm_incremental.cpp', 'src/sparse_sfm_bundle_adjustment.cpp', @@ -200,6 +202,7 @@ executable( '-DLARDON3D_INCREMENTAL_RECONSTRUCTION_TASK_AVAILABLE', '-DLARDON3D_ACQUISITION_CAMPAIGN_TASK_AVAILABLE', '-DLARDON3D_PHOTO_QUALITY_TASK_AVAILABLE', + '-DLARDON3D_RAW_DEVELOPMENT_TASK_AVAILABLE', ], dependencies: [ ncursesw, @@ -330,6 +333,27 @@ raw_development_test = executable( test('raw-development', raw_development_test, timeout: 30) +raw_development_task_test = executable( + 'test-raw-development-task', + sources: [ + 'tests/test_raw_development_task.cpp', + 'src/app_state.c', + 'src/image_catalog.c', 'src/image_view.c', + 'src/image_catalog_persistent.c', + 'src/raw_development.cpp', + 'src/raw_development_task.cpp', + 'src/project.c', + 'src/project_db.c', 'src/project_db_sparse_sfm.c', + 'src/task.c', 'src/task_checkpoint.c', 'src/task_queue.c', + 'src/task_kind_registry.c', + 'src/resource_governor.c', 'src/resource_snapshot.c', + ], + include_directories: include_directories('include'), + dependencies: [threads, sqlite3, openssl, opencv, libraw, libpng, libdeflate], +) + +test('raw-development-task', raw_development_task_test, timeout: 30) + acquisition_pairing_test = executable( 'test-acquisition-pairing', sources: ['tests/test_acquisition_pairing.cpp', 'src/acquisition_pairing.cpp'], @@ -712,6 +736,35 @@ project_db_test = executable( test('project-db', project_db_test, timeout: 30) +selected_execution_test = executable( + 'test-selected-execution', + sources: [ + 'tests/test_selected_execution.c', + 'src/project_db.c', 'src/project_db_sparse_sfm.c', + 'src/task.c', 'src/resource_governor.c', 'src/resource_snapshot.c', + ], + c_args: ['-DLARDON3D_PROJECT_DB_TESTING'], + include_directories: include_directories('include'), + dependencies: [threads, sqlite3, openssl], +) + +test('selected-execution', selected_execution_test, timeout: 30) + +calibration_bootstrap_test = executable( + 'test-calibration-bootstrap', + sources: [ + 'tests/test_calibration_bootstrap.c', + 'src/calibration_bootstrap.c', + 'src/project_db.c', 'src/project_db_sparse_sfm.c', + 'src/task.c', 'src/resource_governor.c', 'src/resource_snapshot.c', + ], + c_args: ['-DLARDON3D_PROJECT_DB_TESTING'], + include_directories: include_directories('include'), + dependencies: [threads, sqlite3, openssl], +) + +test('calibration-bootstrap', calibration_bootstrap_test, timeout: 30) + sparse_sfm_model_test = executable( 'test-sparse-sfm-model', sources: [ diff --git a/src/acquisition_ingest.cpp b/src/acquisition_ingest.cpp index 38f58f1..03b9613 100644 --- a/src/acquisition_ingest.cpp +++ b/src/acquisition_ingest.cpp @@ -158,8 +158,18 @@ extern "C" Lardon3DAcquisitionIngestResult lardon3d_acquisition_ingest( size_t jpeg = SIZE_MAX, raw = SIZE_MAX; for (size_t member = 0; member < group.source_count; ++member) { size_t index = group.source_indices[member]; - database_result = lardon3d_project_db_attach_capture_source_asset( - state->project_db, capture.capture_id, published[index].asset.asset_id); + Lardon3DProjectDbCaptureSourceKind source_kind{}; + if (published[index].metadata.source_kind == LARDON3D_ACQUISITION_SOURCE_JPEG) + source_kind = LARDON3D_DB_CAPTURE_SOURCE_JPEG; + else if (published[index].metadata.source_kind == LARDON3D_ACQUISITION_SOURCE_RAW) + source_kind = LARDON3D_DB_CAPTURE_SOURCE_RAW; + else + return LARDON3D_ACQUISITION_INGEST_CONSTRAINT; + // S3-E commits SOURCE membership with the already validated source kind. + // Downstream RAW work may use only this explicit relation; path, hash, + // basename, attachment order, and image identity cannot replace it. + database_result = lardon3d_project_db_record_capture_source_asset( + state->project_db, capture.capture_id, published[index].asset.asset_id, source_kind); if (database_result != LARDON3D_PROJECT_DB_OK) return db_result(database_result); if (published[index].metadata.source_kind == LARDON3D_ACQUISITION_SOURCE_JPEG && jpeg == SIZE_MAX) jpeg = index; diff --git a/src/calibration_bootstrap.c b/src/calibration_bootstrap.c new file mode 100644 index 0000000..dd579d6 --- /dev/null +++ b/src/calibration_bootstrap.c @@ -0,0 +1,220 @@ +#include + +#include +#include +#include +#include +#include + +enum { + BOOTSTRAP_HEADER_SIZE = 152, + BOOTSTRAP_ENTRY_SIZE = 140, + BOOTSTRAP_VALIDATION_FLAGS = 15, +}; + +static const unsigned char bootstrap_magic[8] = {'L', '3', 'D', 'C', 'A', 'L', 'B', '1'}; + +typedef struct { + const unsigned char *bytes; + size_t remaining; +} BootstrapReader; + +static bool take(BootstrapReader *reader, void *output, size_t count) { + if (count > reader->remaining) return false; + if (output) memcpy(output, reader->bytes, count); + reader->bytes += count; + reader->remaining -= count; + return true; +} + +static bool read_u32(BootstrapReader *reader, uint32_t *output) { + unsigned char bytes[4]; + if (!take(reader, bytes, sizeof(bytes))) return false; + *output = (uint32_t)bytes[0] | ((uint32_t)bytes[1] << 8) | + ((uint32_t)bytes[2] << 16) | ((uint32_t)bytes[3] << 24); + return true; +} + +static bool read_u64(BootstrapReader *reader, uint64_t *output) { + unsigned char bytes[8]; + if (!take(reader, bytes, sizeof(bytes))) return false; + *output = 0; + for (size_t index = 0; index < sizeof(bytes); ++index) + *output |= (uint64_t)bytes[index] << (8u * index); + return true; +} + +static bool read_f64(BootstrapReader *reader, double *output) { + uint64_t bits; + if (!read_u64(reader, &bits)) return false; + memcpy(output, &bits, sizeof(bits)); + if (*output == 0.0) *output = 0.0; + return isfinite(*output); +} + +static bool sha256(const unsigned char *bytes, size_t size, unsigned char output[32]) { + unsigned int output_size = 0; + return EVP_Digest(bytes, size, output, &output_size, EVP_sha256(), NULL) == 1 && + output_size == 32; +} + +static Lardon3DCalibrationBootstrapResult db_result(Lardon3DProjectDbResult result) { + if (result == LARDON3D_PROJECT_DB_CONSTRAINT || result == LARDON3D_PROJECT_DB_NOT_FOUND) + return LARDON3D_CALIBRATION_BOOTSTRAP_SELECTION_CONFLICT; + return LARDON3D_CALIBRATION_BOOTSTRAP_DB_ERROR; +} + +Lardon3DCalibrationBootstrapResult lardon3d_calibration_bootstrap_import( + Lardon3DProjectDb *database, uint64_t execution_id, + const unsigned char *artifact, size_t artifact_size, + const unsigned char expected_artifact_sha256[32], + Lardon3DCalibrationBootstrapOutput *output) { + if (!database || execution_id == 0 || !artifact || !expected_artifact_sha256 || !output || + artifact_size < BOOTSTRAP_HEADER_SIZE || + artifact_size > LARDON3D_CALIBRATION_BOOTSTRAP_MAX_BYTES) + return LARDON3D_CALIBRATION_BOOTSTRAP_INVALID_ARGUMENT; + memset(output, 0, sizeof(*output)); + + unsigned char artifact_sha256[32]; + if (!sha256(artifact, artifact_size, artifact_sha256)) + return LARDON3D_CALIBRATION_BOOTSTRAP_DB_ERROR; + if (memcmp(artifact_sha256, expected_artifact_sha256, 32) != 0) + return LARDON3D_CALIBRATION_BOOTSTRAP_PROVENANCE_MISMATCH; + + Lardon3DProjectDbSelectedExecution execution; + Lardon3DProjectDbResult loaded = + lardon3d_project_db_load_selected_execution(database, execution_id, &execution); + if (loaded != LARDON3D_PROJECT_DB_OK) return db_result(loaded); + if (execution.stage != LARDON3D_SELECTED_EXECUTION_CALIBRATION && + execution.stage != LARDON3D_SELECTED_EXECUTION_READY) + return LARDON3D_CALIBRATION_BOOTSTRAP_SELECTION_CONFLICT; + + BootstrapReader reader = {artifact, artifact_size}; + unsigned char magic[8]; + uint32_t version, model_kind, model_version, entry_count; + unsigned char solver_executable_sha256[32]; + unsigned char solver_configuration_sha256[32]; + unsigned char initialization_evidence_sha256[32]; + unsigned char validation_evidence_sha256[32]; + if (!take(&reader, magic, sizeof(magic)) || + !read_u32(&reader, &version) || !read_u32(&reader, &model_kind) || + !read_u32(&reader, &model_version) || !read_u32(&reader, &entry_count) || + !take(&reader, solver_executable_sha256, 32) || + !take(&reader, solver_configuration_sha256, 32) || + !take(&reader, initialization_evidence_sha256, 32) || + !take(&reader, validation_evidence_sha256, 32) || + memcmp(magic, bootstrap_magic, sizeof(magic)) != 0 || + version != LARDON3D_CALIBRATION_BOOTSTRAP_ARTIFACT_VERSION || + model_kind != LARDON3D_SPARSE_SFM_CALIBRATION_KIND_PINHOLE || + model_version != LARDON3D_SPARSE_SFM_CALIBRATION_VERSION || entry_count == 0 || + entry_count != execution.item_count || + artifact_size != BOOTSTRAP_HEADER_SIZE + (size_t)entry_count * BOOTSTRAP_ENTRY_SIZE) + return LARDON3D_CALIBRATION_BOOTSTRAP_MALFORMED_ARTIFACT; + + /* These four digests bind executable identity, exact solver configuration, + * initialization evidence and validation diagnostics into artifact identity. + * An all-zero digest is not evidence and is therefore rejected. */ + unsigned char executable_or = 0, configuration_or = 0, initialization_or = 0, validation_or = 0; + for (size_t index = 0; index < 32; ++index) { + executable_or |= solver_executable_sha256[index]; + configuration_or |= solver_configuration_sha256[index]; + initialization_or |= initialization_evidence_sha256[index]; + validation_or |= validation_evidence_sha256[index]; + } + if (executable_or == 0 || configuration_or == 0 || initialization_or == 0 || + validation_or == 0) + return LARDON3D_CALIBRATION_BOOTSTRAP_MALFORMED_ARTIFACT; + + Lardon3DSparseCalibration *calibrations = calloc(entry_count, sizeof(*calibrations)); + Lardon3DSparseCalibrationMember *members = calloc(entry_count, sizeof(*members)); + if (!calibrations || !members) { + free(calibrations); + free(members); + return LARDON3D_CALIBRATION_BOOTSTRAP_OUT_OF_MEMORY; + } + + Lardon3DCalibrationBootstrapResult result = LARDON3D_CALIBRATION_BOOTSTRAP_OK; + for (uint32_t index = 0; index < entry_count && result == LARDON3D_CALIBRATION_BOOTSTRAP_OK; + ++index) { + uint64_t image_id; + unsigned char representation_sha256[32]; + uint32_t width, height, support_images, support_observations, validation_flags; + double reprojection_rmse, maximum_parameter_delta; + Lardon3DSparseCalibration *calibration = &calibrations[index]; + if (!read_u64(&reader, &image_id) || !take(&reader, representation_sha256, 32) || + !read_u32(&reader, &width) || !read_u32(&reader, &height) || + !read_f64(&reader, &calibration->fx) || !read_f64(&reader, &calibration->fy) || + !read_f64(&reader, &calibration->cx) || !read_f64(&reader, &calibration->cy) || + !read_f64(&reader, &calibration->k1) || !read_f64(&reader, &calibration->k2) || + !read_f64(&reader, &calibration->p1) || !read_f64(&reader, &calibration->p2) || + !read_u32(&reader, &support_images) || !read_u32(&reader, &support_observations) || + !read_f64(&reader, &reprojection_rmse) || + !read_f64(&reader, &maximum_parameter_delta) || + !read_u32(&reader, &validation_flags) || image_id == 0 || width == 0 || height == 0 || + calibration->fx <= 0.0 || calibration->fy <= 0.0 || calibration->cx < 0.0 || + calibration->cy < 0.0 || calibration->cx >= (double)width || + calibration->cy >= (double)height || support_images == 0 || support_observations == 0 || + reprojection_rmse < 0.0 || maximum_parameter_delta < 0.0 || + validation_flags != BOOTSTRAP_VALIDATION_FLAGS) { + result = LARDON3D_CALIBRATION_BOOTSTRAP_MALFORMED_ARTIFACT; + break; + } + + Lardon3DProjectDbSelectedExecutionItem selected; + Lardon3DProjectDbImage image; + Lardon3DProjectDbImageAsset asset; + loaded = lardon3d_project_db_load_selected_execution_item(database, execution_id, index, + &selected); + if (loaded != LARDON3D_PROJECT_DB_OK || !selected.has_image || selected.image_id != image_id) { + result = LARDON3D_CALIBRATION_BOOTSTRAP_SELECTION_CONFLICT; + break; + } + loaded = lardon3d_project_db_load_image(database, image_id, &image, &asset); + if (loaded != LARDON3D_PROJECT_DB_OK || memcmp(asset.sha256, representation_sha256, 32) != 0) { + result = LARDON3D_CALIBRATION_BOOTSTRAP_SELECTION_CONFLICT; + break; + } + calibration->model_kind = model_kind; + calibration->model_version = model_version; + calibration->width = width; + calibration->height = height; + calibration->provenance_kind = LARDON3D_SPARSE_SFM_PROVENANCE_IMPORTED_TRUSTED; + memcpy(calibration->provenance_fingerprint, artifact_sha256, 32); + members[index].image_id = image_id; + } + if (reader.remaining != 0 && result == LARDON3D_CALIBRATION_BOOTSTRAP_OK) + result = LARDON3D_CALIBRATION_BOOTSTRAP_MALFORMED_ARTIFACT; + + /* Validation precedes publication. The established calibration APIs own + * short transactions; their immutable, content-addressed rows may survive a + * later failure, while READY remains guarded by the final scope attachment. */ + for (uint32_t index = 0; index < entry_count && result == LARDON3D_CALIBRATION_BOOTSTRAP_OK; + ++index) { + Lardon3DSparseCalibration stored; + loaded = lardon3d_sparse_calibration_create(database, &calibrations[index], &stored); + if (loaded != LARDON3D_PROJECT_DB_OK) { + result = db_result(loaded); + break; + } + members[index].calibration_id = stored.calibration_id; + memcpy(members[index].calibration_hash, stored.scientific_hash, 32); + } + Lardon3DSparseCalibrationScope scope; + if (result == LARDON3D_CALIBRATION_BOOTSTRAP_OK) { + loaded = lardon3d_sparse_calibration_scope_create(database, members, entry_count, &scope); + if (loaded != LARDON3D_PROJECT_DB_OK) result = db_result(loaded); + } + if (result == LARDON3D_CALIBRATION_BOOTSTRAP_OK) { + loaded = lardon3d_project_db_assign_selected_calibration_scope(database, execution_id, + scope.scope_id); + if (loaded != LARDON3D_PROJECT_DB_OK) result = db_result(loaded); + } + if (result == LARDON3D_CALIBRATION_BOOTSTRAP_OK) { + memcpy(output->artifact_sha256, artifact_sha256, 32); + output->scope = scope; + output->calibration_count = entry_count; + } + free(calibrations); + free(members); + return result; +} diff --git a/src/project.c b/src/project.c index 94bcc6d..64be368 100644 --- a/src/project.c +++ b/src/project.c @@ -676,7 +676,8 @@ checkpoint_task_internal(Lardon3DAppState *state, const Lardon3DTask *task, const Lardon3DProjectDbMatcherTask *matcher_parameters, const Lardon3DProjectDbGeometricVerifierTask *geometric_parameters, const Lardon3DProjectDbAcquisitionCampaignTask *campaign_parameters, - const Lardon3DProjectDbPhotoQualityTask *quality_parameters) { + const Lardon3DProjectDbPhotoQualityTask *quality_parameters, + const Lardon3DProjectDbRawDevelopmentTask *raw_parameters) { if (!state || !state->project_loaded || !state->project_db) { return LARDON3D_PROJECT_TASK_CHECKPOINT_NO_PROJECT; } @@ -751,6 +752,10 @@ checkpoint_task_internal(Lardon3DAppState *state, const Lardon3DTask *task, ? lardon3d_project_db_record_photo_quality_task( state->project_db, &snapshot, task_kind, task_kind_version, &checkpoint, quality_parameters, now.tv_sec) + : raw_parameters + ? lardon3d_project_db_record_raw_development_task( + state->project_db, &snapshot, task_kind, task_kind_version, &checkpoint, + raw_parameters, now.tv_sec) : lardon3d_project_db_record_task(state->project_db, &snapshot, task_kind, task_kind_version, &checkpoint, now.tv_sec); if (recorded == LARDON3D_PROJECT_DB_BUSY) { @@ -767,7 +772,7 @@ checkpoint_task_internal(Lardon3DAppState *state, const Lardon3DTask *task, Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_task(Lardon3DAppState *state, const Lardon3DTask *task) { return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, - NULL); + NULL, NULL); } Lardon3DProjectTaskCheckpointResult @@ -777,7 +782,7 @@ lardon3d_project_checkpoint_image_import_task(Lardon3DAppState *state, const Lar return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK; } return checkpoint_task_internal( - state, task, source_path, scanset_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + state, task, source_path, scanset_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); } Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_acquisition_campaign_task( @@ -785,7 +790,7 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_acquisition_camp const Lardon3DProjectDbAcquisitionCampaignTask *parameters) { if (!parameters) return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK; return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, - parameters, NULL); + parameters, NULL, NULL); } Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_photo_quality_task( @@ -793,7 +798,15 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_photo_quality_ta const Lardon3DProjectDbPhotoQualityTask *parameters) { if (!parameters) return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK; return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, - parameters); + parameters, NULL); +} + +Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_raw_development_task( + Lardon3DAppState *state, const Lardon3DTask *task, + const Lardon3DProjectDbRawDevelopmentTask *parameters) { + if (!parameters) return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK; + return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, parameters); } Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_feature_extract_task( @@ -803,7 +816,7 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_feature_extract_ return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK; } return checkpoint_task_internal(state, task, NULL, 0, parameters, NULL, NULL, NULL, NULL, NULL, - NULL, NULL); + NULL, NULL, NULL); } Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_sift_extract_task( @@ -811,7 +824,7 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_sift_extract_tas const Lardon3DProjectDbSiftExtractTask *parameters) { if (!parameters) return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK; return checkpoint_task_internal(state, task, NULL, 0, NULL, parameters, NULL, NULL, NULL, NULL, - NULL, NULL); + NULL, NULL, NULL); } Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_visual_index_update_task( @@ -821,7 +834,7 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_visual_index_upd return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK; } return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, parameters, NULL, NULL, NULL, - NULL, NULL); + NULL, NULL, NULL); } Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_candidate_pair_generate_task( @@ -831,7 +844,7 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_candidate_pair_g return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK; } return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, parameters, NULL, NULL, - NULL, NULL); + NULL, NULL, NULL); } Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_matcher_task( @@ -841,7 +854,7 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_matcher_task( return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK; } return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, parameters, NULL, - NULL, NULL); + NULL, NULL, NULL); } Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_geometric_verifier_task( @@ -851,7 +864,7 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_geometric_verifi return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK; } return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, NULL, - parameters, NULL, NULL); + parameters, NULL, NULL, NULL); } static bool coherent_recovery(const Lardon3DProjectDbTask *database_task, diff --git a/src/project_db.c b/src/project_db.c index 293ab89..9d2cee3 100644 --- a/src/project_db.c +++ b/src/project_db.c @@ -12,6 +12,7 @@ #include #include +#include #include "project_db_internal.h" static const char schema_v5[] = @@ -649,6 +650,64 @@ static const char schema_photo_quality_v21[] = "low_texture_fraction REAL NOT NULL,reasons TEXT NOT NULL CHECK(length(reasons)<256)," "PRIMARY KEY(task_id,group_id));"; +static const char schema_selected_execution_v22[] = + /* v22 makes the cross-request identity bridge explicit. Group IDs remain + operational identities scoped to their respective typed Task. */ + /* Legacy v21 capture_assets rows are intentionally not backfilled: their + source kind is unavailable and must never be guessed. */ + "CREATE TABLE IF NOT EXISTS capture_source_assets(" + "capture_id INTEGER NOT NULL,asset_id INTEGER NOT NULL," + "source_kind INTEGER NOT NULL CHECK(source_kind IN(1,2))," + "PRIMARY KEY(capture_id,asset_id)," + "FOREIGN KEY(capture_id,asset_id) REFERENCES capture_assets(capture_id,asset_id) " + "ON DELETE CASCADE);" + "CREATE INDEX IF NOT EXISTS capture_source_assets_page_idx ON " + "capture_source_assets(capture_id,asset_id);" + "CREATE TABLE IF NOT EXISTS selected_executions(" + "execution_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(execution_id>0)," + "quality_task_id INTEGER NOT NULL REFERENCES photo_quality_triage_tasks(task_id)," + "campaign_task_id INTEGER NOT NULL REFERENCES acquisition_campaign_tasks(task_id)," + "calibration_scope_id INTEGER REFERENCES sparse_calibration_scopes(scope_id)," + "stage INTEGER NOT NULL CHECK(stage BETWEEN 1 AND 3)," + "next_item_index INTEGER NOT NULL CHECK(next_item_index>=0 AND next_item_index<=4096)," + "item_count INTEGER NOT NULL CHECK(item_count>0 AND item_count<=4096)," + "created_at INTEGER NOT NULL CHECK(created_at>=0)," + "CHECK(next_item_index<=item_count)," + "CHECK((stage=1 AND next_item_index=0 AND item_index<4096)," + "quality_task_id INTEGER NOT NULL,campaign_task_id INTEGER NOT NULL," + "quality_group_id INTEGER NOT NULL CHECK(quality_group_id>0 AND quality_group_id<=4096)," + "campaign_group_id INTEGER NOT NULL CHECK(campaign_group_id>0 AND campaign_group_id<=4096)," + "capture_id INTEGER NOT NULL,representation_source INTEGER NOT NULL " + "CHECK(representation_source IN(1,2)),source_asset_id INTEGER,image_id INTEGER," + "CHECK((representation_source=1 AND source_asset_id IS NULL) OR " + "(representation_source=2 AND source_asset_id IS NOT NULL))," + "PRIMARY KEY(execution_id,item_index),UNIQUE(execution_id,quality_group_id)," + "UNIQUE(execution_id,campaign_group_id),UNIQUE(execution_id,capture_id)," + "FOREIGN KEY(execution_id,quality_task_id,campaign_task_id) REFERENCES " + "selected_executions(execution_id,quality_task_id,campaign_task_id) ON DELETE CASCADE," + "FOREIGN KEY(quality_task_id,quality_group_id) REFERENCES " + "photo_quality_triage_results(task_id,group_id)," + "FOREIGN KEY(campaign_task_id,campaign_group_id) REFERENCES " + "acquisition_campaign_captures(task_id,group_id)," + "FOREIGN KEY(capture_id,source_asset_id) REFERENCES " + "capture_source_assets(capture_id,asset_id)," + "FOREIGN KEY(capture_id,image_id) REFERENCES capture_images(capture_id,image_id));" + "CREATE TABLE IF NOT EXISTS raw_development_tasks(" + "task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) ON DELETE CASCADE," + "capture_id INTEGER NOT NULL,source_asset_id INTEGER NOT NULL," + "phase INTEGER NOT NULL CHECK(phase IN(1,2)),image_id INTEGER," + "CHECK((phase=1 AND image_id IS NULL) OR (phase=2 AND image_id IS NOT NULL))," + "FOREIGN KEY(capture_id,source_asset_id) REFERENCES " + "capture_source_assets(capture_id,asset_id)," + "FOREIGN KEY(capture_id,image_id) REFERENCES capture_images(capture_id,image_id));"; + static void copy_error(char destination[LARDON3D_PROJECT_DB_ERROR_CAPACITY], const char *text) { if (destination) { (void)snprintf(destination, LARDON3D_PROJECT_DB_ERROR_CAPACITY, "%s", text ? text : ""); @@ -741,7 +800,7 @@ static Lardon3DProjectDbResult migrate(Lardon3DProjectDb *database, unsigned int from_version != 8 && from_version != 9 && from_version != 10 && from_version != 11 && from_version != 12 && from_version != 13 && from_version != 14 && from_version != 15 && from_version != 16 && from_version != 17 && from_version != 18 && - from_version != 19 && from_version != 20) { + from_version != 19 && from_version != 20 && from_version != 21) { return LARDON3D_PROJECT_DB_CORRUPT; } Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin migration"); @@ -1163,6 +1222,22 @@ static Lardon3DProjectDbResult migrate(Lardon3DProjectDb *database, unsigned int "finish schema v21 migration"); } } + if (result == LARDON3D_PROJECT_DB_OK && from_version < 22) { + result = execute(database, schema_selected_execution_v22, + "migrate selected execution v21 to v22"); +#ifdef LARDON3D_PROJECT_DB_TESTING + if (result == LARDON3D_PROJECT_DB_OK && + getenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V22")) { + result = execute(database, "INSERT INTO missing_v22_test_table VALUES(1)", + "forced migration v22 failure"); + } +#endif + if (result == LARDON3D_PROJECT_DB_OK) { + result = execute(database, + "UPDATE metadata SET value=22 WHERE key='schema_version' AND value=21", + "finish schema v22 migration"); + } + } if (result == LARDON3D_PROJECT_DB_OK) { result = execute(database, "COMMIT", "commit migration"); } @@ -1297,12 +1372,16 @@ Lardon3DProjectDbResult lardon3d_project_db_open(const char *path, Lardon3DProje "captures", "capture_images", "capture_assets", + "capture_source_assets", + "raw_development_tasks", "capture_selections", "asset_derivations", "acquisition_campaign_tasks", "acquisition_campaign_captures", "photo_quality_triage_tasks", - "photo_quality_triage_results"}; + "photo_quality_triage_results", + "selected_executions", + "selected_execution_items"}; for (size_t index = 0; index < sizeof(required) / sizeof(required[0]) && result == LARDON3D_PROJECT_DB_OK; ++index) { @@ -1528,6 +1607,7 @@ record_task_internal(Lardon3DProjectDb *database, const Lardon3DTaskDurableSnaps const Lardon3DProjectDbSparseSfmTask *sparse_sfm, const Lardon3DProjectDbAcquisitionCampaignTask *campaign, const Lardon3DProjectDbPhotoQualityTask *photo_quality, + const Lardon3DProjectDbRawDevelopmentTask *raw_development, int64_t updated_at) { bool typed = task_kind != NULL; if (!database || !valid_durable_task(snapshot, updated_at) || @@ -1535,6 +1615,12 @@ record_task_internal(Lardon3DProjectDb *database, const Lardon3DTaskDurableSnaps (!typed && task_kind_version != 0) || (source_path && !bounded_text(source_path, LARDON3D_PROJECT_DB_PATH_CAPACITY, false)) || (source_path && !valid_task_id(scanset_id)) || + /* Raw development durability must remain exactly paired with its public + * raw.develop durable signature, because this invariant is the only + * crash-safe boundary for reconstructing the typed state after restart. */ + (raw_development && + (!task_kind || strcmp(task_kind, LARDON3D_RAW_DEVELOPMENT_TASK_KIND) != 0 || + task_kind_version != LARDON3D_RAW_DEVELOPMENT_TASK_KIND_VERSION)) || (feature && (!valid_task_id(feature->task_id) || feature->task_id != snapshot->id || !valid_task_id(feature->image_id) || @@ -1605,6 +1691,17 @@ record_task_internal(Lardon3DProjectDb *database, const Lardon3DTaskDurableSnaps photo_quality->group_count > 4096 || photo_quality->next_group_id == 0 || photo_quality->next_group_id > photo_quality->group_count + 1u || !photo_quality->request || photo_quality->request_size == 0 || photo_quality->request_size > INT_MAX)) || + (raw_development && + (!valid_task_id(raw_development->task_id) || + raw_development->task_id != snapshot->id || + !valid_task_id(raw_development->capture_id) || + !valid_task_id(raw_development->source_asset_id) || + (raw_development->phase != LARDON3D_RAW_DEVELOPMENT_TASK_PENDING && + raw_development->phase != LARDON3D_RAW_DEVELOPMENT_TASK_PUBLISHED) || + (raw_development->phase == LARDON3D_RAW_DEVELOPMENT_TASK_PENDING && + (raw_development->has_image || raw_development->image_id != 0)) || + (raw_development->phase == LARDON3D_RAW_DEVELOPMENT_TASK_PUBLISHED && + (!raw_development->has_image || !valid_task_id(raw_development->image_id))))) || (checkpoint && !valid_checkpoint(checkpoint))) { return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; } @@ -1714,6 +1811,37 @@ record_task_internal(Lardon3DProjectDb *database, const Lardon3DTaskDurableSnaps } } } + if (result == LARDON3D_PROJECT_DB_OK && raw_development) { + /* Capture and SOURCE RAW are immutable Task identity. Only the publication + phase advances, and it can advance only to an image already retained by + that Capture. This is the typed half of the atomic restart checkpoint. */ + result = prepare(database, + "INSERT INTO raw_development_tasks(task_id,capture_id,source_asset_id,phase,image_id) " + "SELECT ?1,?2,?3,?4,?5 WHERE EXISTS(SELECT 1 FROM capture_source_assets " + "WHERE capture_id=?2 AND asset_id=?3 AND source_kind=2) AND " + "(?4=1 OR EXISTS(SELECT 1 FROM capture_images WHERE capture_id=?2 AND image_id=?5)) " + "ON CONFLICT(task_id) DO UPDATE SET phase=excluded.phase,image_id=excluded.image_id " + "WHERE raw_development_tasks.capture_id=excluded.capture_id AND " + "raw_development_tasks.source_asset_id=excluded.source_asset_id AND " + "excluded.phase>=raw_development_tasks.phase AND " + "(raw_development_tasks.phase<2 OR raw_development_tasks.image_id=excluded.image_id)", + &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_int64(statement, 1, (sqlite3_int64)raw_development->task_id); + sqlite3_bind_int64(statement, 2, (sqlite3_int64)raw_development->capture_id); + sqlite3_bind_int64(statement, 3, (sqlite3_int64)raw_development->source_asset_id); + sqlite3_bind_int(statement, 4, (int)raw_development->phase); + if (raw_development->has_image) + sqlite3_bind_int64(statement, 5, (sqlite3_int64)raw_development->image_id); + else + sqlite3_bind_null(statement, 5); + result = step_done(database, statement, "upsert RAW development task"); + if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1) { + copy_error(database->error, "Identité ou phase de développement RAW incohérente."); + result = LARDON3D_PROJECT_DB_CONSTRAINT; + } + } + } if (result == LARDON3D_PROJECT_DB_OK) { result = prepare(database, @@ -2048,7 +2176,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_task( Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot, const char *task_kind, uint32_t task_kind_version, const Lardon3DProjectDbCheckpoint *checkpoint, int64_t updated_at) { return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, NULL, 0, - NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, updated_at); + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, updated_at); } Lardon3DProjectDbResult lardon3d_project_db_record_image_import_task( @@ -2060,7 +2188,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_image_import_task( } return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, source_path, scanset_id, NULL, NULL, NULL, NULL, NULL, NULL, - NULL, NULL, NULL, updated_at); + NULL, NULL, NULL, NULL, updated_at); } Lardon3DProjectDbResult lardon3d_project_db_record_feature_extract_task( @@ -2071,7 +2199,8 @@ Lardon3DProjectDbResult lardon3d_project_db_record_feature_extract_task( return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; } return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, NULL, 0, - parameters, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, updated_at); + parameters, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + updated_at); } Lardon3DProjectDbResult lardon3d_project_db_record_sift_extract_task( @@ -2081,7 +2210,8 @@ Lardon3DProjectDbResult lardon3d_project_db_record_sift_extract_task( const Lardon3DProjectDbSiftExtractTask *parameters, int64_t updated_at) { if (!parameters) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, NULL, 0, - NULL, parameters, NULL, NULL, NULL, NULL, NULL, NULL, NULL, updated_at); + NULL, parameters, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + updated_at); } static bool read_task(sqlite3_stmt *statement, Lardon3DProjectDbTask *task) { @@ -2326,7 +2456,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_acquisition_campaign_task( never observes a valid campaign Task without its immutable request blob. */ return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, parameters, NULL, - updated_at); + NULL, updated_at); } Lardon3DProjectDbResult lardon3d_project_db_load_acquisition_campaign_task( @@ -2341,7 +2471,7 @@ Lardon3DProjectDbResult lardon3d_project_db_load_acquisition_campaign_task( "SELECT scanset_id,next_group_id,group_count,request FROM acquisition_campaign_tasks " "WHERE task_id=?1", &statement); if (result == LARDON3D_PROJECT_DB_OK) { - sqlite3_bind_int64(statement, 1, (sqlite3_int64)task_id); + sqlite3_bind_int64(statement, 1, (sqlite3_int64)task_id); int code = sqlite3_step(statement); if (code == SQLITE_DONE) result = LARDON3D_PROJECT_DB_NOT_FOUND; else if (code != SQLITE_ROW) result = sqlite_result(database, code, "load campaign task"); @@ -2480,7 +2610,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_photo_quality_task( /* Generic runtime state and typed immutable request share the transaction; recovery cannot observe one without the other. */ return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, NULL, 0, - NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, parameters, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, parameters, NULL, updated_at); } @@ -2535,6 +2665,80 @@ Lardon3DProjectDbResult lardon3d_project_db_load_photo_quality_task( return result; } +Lardon3DProjectDbResult lardon3d_project_db_record_raw_development_task( + Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot, + const char *task_kind, uint32_t task_kind_version, + const Lardon3DProjectDbCheckpoint *checkpoint, + const Lardon3DProjectDbRawDevelopmentTask *parameters, int64_t updated_at) { + if (!parameters) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, NULL, 0, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, parameters, + updated_at); +} + +Lardon3DProjectDbResult lardon3d_project_db_load_raw_development_task( + Lardon3DProjectDb *database, uint64_t task_id, + Lardon3DProjectDbRawDevelopmentTask *parameters) { + if (!database || !valid_task_id(task_id) || !parameters) + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + memset(parameters, 0, sizeof(*parameters)); + (void)pthread_mutex_lock(&database->mutex); + sqlite3_stmt *statement = NULL; + Lardon3DProjectDbResult result = prepare(database, + "SELECT r.capture_id,r.source_asset_id,r.phase,r.image_id,cs.source_kind,ci.image_id," + "t.task_kind,t.task_kind_version " + "FROM raw_development_tasks r JOIN tasks t ON t.task_id=r.task_id " + "LEFT JOIN capture_source_assets cs ON " + "cs.capture_id=r.capture_id AND cs.asset_id=r.source_asset_id " + "LEFT JOIN capture_images ci ON ci.capture_id=r.capture_id AND ci.image_id=r.image_id " + "WHERE r.task_id=?1", &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_int64(statement, 1, (sqlite3_int64)task_id); + int code = sqlite3_step(statement); + if (code == SQLITE_DONE) result = LARDON3D_PROJECT_DB_NOT_FOUND; + else if (code != SQLITE_ROW) result = sqlite_result(database, code, "load RAW development task"); + else { + sqlite3_int64 capture = sqlite3_column_int64(statement, 0); + sqlite3_int64 source = sqlite3_column_int64(statement, 1); + sqlite3_int64 phase = sqlite3_column_int64(statement, 2); + bool has_image = sqlite3_column_type(statement, 3) != SQLITE_NULL; + sqlite3_int64 image = sqlite3_column_int64(statement, 3); + const unsigned char *persisted_kind = sqlite3_column_text(statement, 6); + sqlite3_int64 persisted_kind_version = sqlite3_column_int64(statement, 7); + bool valid_types = sqlite3_column_type(statement, 0) == SQLITE_INTEGER && + sqlite3_column_type(statement, 1) == SQLITE_INTEGER && + sqlite3_column_type(statement, 2) == SQLITE_INTEGER && + sqlite3_column_type(statement, 6) == SQLITE_TEXT && + sqlite3_column_type(statement, 7) == SQLITE_INTEGER && + sqlite3_column_type(statement, 4) == SQLITE_INTEGER; + bool published = phase == LARDON3D_RAW_DEVELOPMENT_TASK_PUBLISHED; + if (!valid_types || capture <= 0 || source <= 0 || + /* If generic task kind/version diverges from raw.develop/1, + * treat the persisted pair as corrupt rather than attempting recovery. */ + !persisted_kind || + strcmp((const char *)persisted_kind, LARDON3D_RAW_DEVELOPMENT_TASK_KIND) != 0 || + persisted_kind_version != LARDON3D_RAW_DEVELOPMENT_TASK_KIND_VERSION || + sqlite3_column_int64(statement, 4) != LARDON3D_DB_CAPTURE_SOURCE_RAW || + (phase != LARDON3D_RAW_DEVELOPMENT_TASK_PENDING && !published) || + (published != has_image) || (published && + (image <= 0 || sqlite3_column_type(statement, 5) != SQLITE_INTEGER || + sqlite3_column_int64(statement, 5) != image))) { + result = LARDON3D_PROJECT_DB_CORRUPT; + } else { + parameters->task_id = task_id; + parameters->capture_id = (uint64_t)capture; + parameters->source_asset_id = (uint64_t)source; + parameters->phase = (Lardon3DProjectDbRawDevelopmentTaskPhase)phase; + parameters->has_image = has_image; + parameters->image_id = has_image ? (uint64_t)image : 0; + } + } + sqlite3_finalize(statement); + } + (void)pthread_mutex_unlock(&database->mutex); + return result; +} + static bool valid_quality_result(const Lardon3DProjectDbPhotoQualityResult *r) { const Lardon3DPhotoQualityMetrics *m = r ? &r->metrics : NULL; bool decoded_pair = m && ((m->decoded_width == 0) == (m->decoded_height == 0)); @@ -2780,6 +2984,353 @@ Lardon3DProjectDbResult lardon3d_project_db_set_photo_quality_override( return result; } +static bool read_selected_execution_row(sqlite3_stmt *statement, + Lardon3DProjectDbSelectedExecution *execution) { + for (int column = 0; column < 8; ++column) { + if (column != 3 && sqlite3_column_type(statement, column) != SQLITE_INTEGER) + return false; + } + sqlite3_int64 id = sqlite3_column_int64(statement, 0); + sqlite3_int64 quality = sqlite3_column_int64(statement, 1); + sqlite3_int64 campaign = sqlite3_column_int64(statement, 2); + bool has_scope = sqlite3_column_type(statement, 3) != SQLITE_NULL; + sqlite3_int64 scope = has_scope ? sqlite3_column_int64(statement, 3) : 0; + sqlite3_int64 stage = sqlite3_column_int64(statement, 4); + sqlite3_int64 next = sqlite3_column_int64(statement, 5); + sqlite3_int64 count = sqlite3_column_int64(statement, 6); + sqlite3_int64 created = sqlite3_column_int64(statement, 7); + bool relation = (stage == LARDON3D_SELECTED_EXECUTION_REPRESENTATIONS && !has_scope && + next < count) || + (stage == LARDON3D_SELECTED_EXECUTION_CALIBRATION && !has_scope && + next == count) || + (stage == LARDON3D_SELECTED_EXECUTION_READY && has_scope && next == count); + if (id <= 0 || quality <= 0 || campaign <= 0 || (has_scope && scope <= 0) || + stage < LARDON3D_SELECTED_EXECUTION_REPRESENTATIONS || + stage > LARDON3D_SELECTED_EXECUTION_READY || next < 0 || count <= 0 || count > 4096 || + next > count || created < 0 || !relation) + return false; + memset(execution, 0, sizeof(*execution)); + execution->execution_id = (uint64_t)id; + execution->quality_task_id = (uint64_t)quality; + execution->campaign_task_id = (uint64_t)campaign; + execution->has_calibration_scope = has_scope; + execution->calibration_scope_id = (uint64_t)scope; + execution->stage = (Lardon3DProjectDbSelectedExecutionStage)stage; + execution->next_item_index = (uint32_t)next; + execution->item_count = (uint32_t)count; + execution->created_at = created; + return true; +} + +static Lardon3DProjectDbResult load_selected_execution_locked( + Lardon3DProjectDb *database, uint64_t execution_id, + Lardon3DProjectDbSelectedExecution *execution) { + sqlite3_stmt *statement = NULL; + Lardon3DProjectDbResult result = prepare( + database, "SELECT execution_id,quality_task_id,campaign_task_id,calibration_scope_id," + "stage,next_item_index,item_count,created_at FROM selected_executions " + "WHERE execution_id=?1", &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_int64(statement, 1, (sqlite3_int64)execution_id); + int code = sqlite3_step(statement); + if (code == SQLITE_DONE) result = LARDON3D_PROJECT_DB_NOT_FOUND; + else if (code != SQLITE_ROW) result = sqlite_result(database, code, "load selected execution"); + else if (!read_selected_execution_row(statement, execution)) + result = LARDON3D_PROJECT_DB_CORRUPT; + sqlite3_finalize(statement); + } + return result; +} + +Lardon3DProjectDbResult lardon3d_project_db_load_selected_execution( + Lardon3DProjectDb *database, uint64_t execution_id, + Lardon3DProjectDbSelectedExecution *execution) { + if (!database || !valid_task_id(execution_id) || !execution) + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + memset(execution, 0, sizeof(*execution)); + (void)pthread_mutex_lock(&database->mutex); + Lardon3DProjectDbResult result = + load_selected_execution_locked(database, execution_id, execution); + (void)pthread_mutex_unlock(&database->mutex); + return result; +} + +Lardon3DProjectDbResult lardon3d_project_db_load_selected_execution_item( + Lardon3DProjectDb *database, uint64_t execution_id, uint32_t item_index, + Lardon3DProjectDbSelectedExecutionItem *item) { + if (!database || !valid_task_id(execution_id) || !item) + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + memset(item, 0, sizeof(*item)); + (void)pthread_mutex_lock(&database->mutex); + sqlite3_stmt *statement = NULL; + Lardon3DProjectDbResult result = prepare( + database, "SELECT i.item_index,i.quality_group_id,i.campaign_group_id,i.capture_id," + "i.representation_source,i.source_asset_id,i.image_id,e.item_count " + "FROM selected_execution_items i JOIN selected_executions e " + "ON e.execution_id=i.execution_id WHERE i.execution_id=?1 AND i.item_index=?2", + &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_int64(statement, 1, (sqlite3_int64)execution_id); + sqlite3_bind_int64(statement, 2, item_index); + int code = sqlite3_step(statement); + if (code == SQLITE_DONE) result = LARDON3D_PROJECT_DB_NOT_FOUND; + else if (code != SQLITE_ROW) result = sqlite_result(database, code, "load selected item"); + else { + bool has_source_asset = sqlite3_column_type(statement, 5) != SQLITE_NULL; + bool has_image = sqlite3_column_type(statement, 6) != SQLITE_NULL; + sqlite3_int64 index = sqlite3_column_int64(statement, 0); + sqlite3_int64 quality = sqlite3_column_int64(statement, 1); + sqlite3_int64 campaign = sqlite3_column_int64(statement, 2); + sqlite3_int64 capture = sqlite3_column_int64(statement, 3); + sqlite3_int64 source_kind = sqlite3_column_int64(statement, 4); + sqlite3_int64 source_asset = has_source_asset ? sqlite3_column_int64(statement, 5) : 0; + sqlite3_int64 image = has_image ? sqlite3_column_int64(statement, 6) : 0; + sqlite3_int64 count = sqlite3_column_int64(statement, 7); + if (sqlite3_column_type(statement, 0) != SQLITE_INTEGER || + sqlite3_column_type(statement, 1) != SQLITE_INTEGER || + sqlite3_column_type(statement, 2) != SQLITE_INTEGER || + sqlite3_column_type(statement, 3) != SQLITE_INTEGER || + sqlite3_column_type(statement, 4) != SQLITE_INTEGER || + sqlite3_column_type(statement, 7) != SQLITE_INTEGER || index < 0 || index >= count || + count <= 0 || count > 4096 || quality <= 0 || quality > 4096 || campaign <= 0 || + campaign > 4096 || capture <= 0 || + source_kind < LARDON3D_SELECTED_REPRESENTATION_SOURCE_IMAGE || + source_kind > LARDON3D_SELECTED_REPRESENTATION_RAW_ASSET || + (source_kind == LARDON3D_SELECTED_REPRESENTATION_SOURCE_IMAGE && has_source_asset) || + (source_kind == LARDON3D_SELECTED_REPRESENTATION_RAW_ASSET && + (!has_source_asset || source_asset <= 0)) || (has_image && image <= 0)) { + result = LARDON3D_PROJECT_DB_CORRUPT; + } else { + item->item_index = (uint32_t)index; + item->quality_group_id = (uint32_t)quality; + item->campaign_group_id = (uint32_t)campaign; + item->capture_id = (uint64_t)capture; + item->representation_source = (Lardon3DProjectDbSelectedRepresentationSource)source_kind; + item->source_asset_id = (uint64_t)source_asset; + item->has_image = has_image; + item->image_id = (uint64_t)image; + } + } + sqlite3_finalize(statement); + } + (void)pthread_mutex_unlock(&database->mutex); + return result; +} + +static Lardon3DProjectDbResult compare_selected_items_locked( + Lardon3DProjectDb *database, uint64_t execution_id, + const Lardon3DProjectDbSelectedExecutionItem *items, size_t item_count) { + sqlite3_stmt *statement = NULL; + Lardon3DProjectDbResult result = prepare( + database, "SELECT item_index,quality_group_id,campaign_group_id,capture_id," + "representation_source,source_asset_id,image_id " + "FROM selected_execution_items WHERE execution_id=?1 ORDER BY item_index", + &statement); + if (result != LARDON3D_PROJECT_DB_OK) return result; + sqlite3_bind_int64(statement, 1, (sqlite3_int64)execution_id); + size_t index = 0; + int code; + while ((code = sqlite3_step(statement)) == SQLITE_ROW && index < item_count) { + if (sqlite3_column_int64(statement, 0) != (sqlite3_int64)index || + sqlite3_column_int64(statement, 1) != items[index].quality_group_id || + sqlite3_column_int64(statement, 2) != items[index].campaign_group_id || + sqlite3_column_int64(statement, 3) != (sqlite3_int64)items[index].capture_id || + sqlite3_column_int64(statement, 4) != items[index].representation_source || + ((items[index].representation_source == LARDON3D_SELECTED_REPRESENTATION_SOURCE_IMAGE && + sqlite3_column_type(statement, 5) != SQLITE_NULL) || + (items[index].representation_source == LARDON3D_SELECTED_REPRESENTATION_RAW_ASSET && + (sqlite3_column_type(statement, 5) != SQLITE_INTEGER || + sqlite3_column_int64(statement, 5) != (sqlite3_int64)items[index].source_asset_id)))) { + result = LARDON3D_PROJECT_DB_CONSTRAINT; + break; + } + ++index; + } + if (result == LARDON3D_PROJECT_DB_OK && (code != SQLITE_DONE || index != item_count)) + result = code == SQLITE_ROW || code == SQLITE_DONE + ? LARDON3D_PROJECT_DB_CONSTRAINT + : sqlite_result(database, code, "compare selected items"); + sqlite3_finalize(statement); + return result; +} + +Lardon3DProjectDbResult lardon3d_project_db_create_selected_execution( + Lardon3DProjectDb *database, uint64_t quality_task_id, + uint64_t campaign_task_id, const Lardon3DProjectDbSelectedExecutionItem *items, + size_t item_count, int64_t created_at, + Lardon3DProjectDbSelectedExecution *execution) { + if (!database || !valid_task_id(quality_task_id) || !valid_task_id(campaign_task_id) || + !items || item_count == 0 || item_count > 4096 || created_at < 0 || !execution) + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + for (size_t index = 0; index < item_count; ++index) { + if (items[index].item_index != index || items[index].quality_group_id == 0 || + items[index].campaign_group_id == 0 || !valid_task_id(items[index].capture_id) || + (items[index].representation_source != LARDON3D_SELECTED_REPRESENTATION_SOURCE_IMAGE && + items[index].representation_source != LARDON3D_SELECTED_REPRESENTATION_RAW_ASSET) || + (items[index].representation_source == LARDON3D_SELECTED_REPRESENTATION_SOURCE_IMAGE && + items[index].source_asset_id != 0) || + (items[index].representation_source == LARDON3D_SELECTED_REPRESENTATION_RAW_ASSET && + !valid_task_id(items[index].source_asset_id)) || + items[index].has_image || items[index].image_id != 0) + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + } + memset(execution, 0, sizeof(*execution)); + (void)pthread_mutex_lock(&database->mutex); + Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin selected snapshot"); + sqlite3_stmt *statement = NULL; + uint64_t existing_id = 0; + bool created_new = false; + if (result == LARDON3D_PROJECT_DB_OK) { + result = prepare(database, "SELECT execution_id,item_count,created_at FROM selected_executions " + "WHERE quality_task_id=?1 AND campaign_task_id=?2", &statement); + } + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_int64(statement, 1, (sqlite3_int64)quality_task_id); + sqlite3_bind_int64(statement, 2, (sqlite3_int64)campaign_task_id); + int code = sqlite3_step(statement); + if (code == SQLITE_ROW) { + existing_id = (uint64_t)sqlite3_column_int64(statement, 0); + if (sqlite3_column_int64(statement, 1) != (sqlite3_int64)item_count || + sqlite3_column_int64(statement, 2) != created_at) + result = LARDON3D_PROJECT_DB_CONSTRAINT; + } else if (code != SQLITE_DONE) { + result = sqlite_result(database, code, "find selected snapshot"); + } + sqlite3_finalize(statement); + statement = NULL; + } + if (result == LARDON3D_PROJECT_DB_OK && existing_id != 0) + result = compare_selected_items_locked(database, existing_id, items, item_count); + if (result == LARDON3D_PROJECT_DB_OK && existing_id == 0) { + result = prepare(database, + "INSERT INTO selected_executions(quality_task_id,campaign_task_id,stage,next_item_index," + "item_count,created_at) SELECT ?1,?2,1,0,?3,?4 WHERE EXISTS(SELECT 1 FROM " + "photo_quality_triage_tasks q JOIN acquisition_campaign_tasks c ON q.scanset_id=c.scanset_id " + "WHERE q.task_id=?1 AND c.task_id=?2 AND q.next_group_id=q.group_count+1)", &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_int64(statement, 1, (sqlite3_int64)quality_task_id); + sqlite3_bind_int64(statement, 2, (sqlite3_int64)campaign_task_id); + sqlite3_bind_int64(statement, 3, (sqlite3_int64)item_count); + sqlite3_bind_int64(statement, 4, created_at); + result = step_done(database, statement, "create selected snapshot"); + if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1) + result = LARDON3D_PROJECT_DB_CONSTRAINT; + } + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_int64 id = sqlite3_last_insert_rowid(database->connection); + if (id <= 0) result = LARDON3D_PROJECT_DB_CORRUPT; + else { + existing_id = (uint64_t)id; + created_new = true; + } + } + } + for (size_t index = 0; result == LARDON3D_PROJECT_DB_OK && created_new && + index < item_count; ++index) { + result = prepare(database, + "INSERT INTO selected_execution_items(execution_id,item_index,quality_task_id," + "campaign_task_id,quality_group_id,campaign_group_id,capture_id," + "representation_source,source_asset_id) " + "SELECT ?1,?2,?3,?4,?5,?6,?7,?8,?9 WHERE EXISTS(SELECT 1 FROM " + "photo_quality_triage_results q JOIN acquisition_campaign_captures c " + "ON c.task_id=?4 AND c.group_id=?6 AND c.capture_id=?7 " + "WHERE q.task_id=?3 AND q.group_id=?5 AND " + "(q.human_override=1 OR (q.human_override=0 AND q.recommendation=1))) " + "AND (?8=1 OR EXISTS(SELECT 1 FROM capture_source_assets ca WHERE ca.capture_id=?7 " + "AND ca.asset_id=?9 AND ca.source_kind=2))", &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_int64(statement, 1, (sqlite3_int64)existing_id); + sqlite3_bind_int64(statement, 2, (sqlite3_int64)index); + sqlite3_bind_int64(statement, 3, (sqlite3_int64)quality_task_id); + sqlite3_bind_int64(statement, 4, (sqlite3_int64)campaign_task_id); + sqlite3_bind_int64(statement, 5, items[index].quality_group_id); + sqlite3_bind_int64(statement, 6, items[index].campaign_group_id); + sqlite3_bind_int64(statement, 7, (sqlite3_int64)items[index].capture_id); + sqlite3_bind_int(statement, 8, (int)items[index].representation_source); + if (items[index].representation_source == LARDON3D_SELECTED_REPRESENTATION_RAW_ASSET) + sqlite3_bind_int64(statement, 9, (sqlite3_int64)items[index].source_asset_id); + else + sqlite3_bind_null(statement, 9); + result = step_done(database, statement, "insert selected item"); + if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1) + result = LARDON3D_PROJECT_DB_CONSTRAINT; + } + } + if (result == LARDON3D_PROJECT_DB_OK) + result = execute(database, "COMMIT", "commit selected snapshot"); + else + (void)execute(database, "ROLLBACK", "rollback selected snapshot"); + if (result == LARDON3D_PROJECT_DB_OK) + result = load_selected_execution_locked(database, existing_id, execution); + (void)pthread_mutex_unlock(&database->mutex); + return result; +} + +Lardon3DProjectDbResult lardon3d_project_db_record_selected_representation( + Lardon3DProjectDb *database, uint64_t execution_id, uint32_t item_index, + uint64_t image_id, uint32_t next_item_index) { + if (!database || !valid_task_id(execution_id) || !valid_task_id(image_id) || + item_index == UINT32_MAX || next_item_index != item_index + 1u) + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + (void)pthread_mutex_lock(&database->mutex); + Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin representation"); + sqlite3_stmt *statement = NULL; + if (result == LARDON3D_PROJECT_DB_OK) + result = prepare(database, + "UPDATE selected_execution_items SET image_id=?3 WHERE execution_id=?1 AND item_index=?2 " + "AND (image_id IS NULL OR image_id=?3) AND EXISTS(SELECT 1 FROM capture_images ci " + "WHERE ci.capture_id=selected_execution_items.capture_id AND ci.image_id=?3)", &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_int64(statement, 1, (sqlite3_int64)execution_id); + sqlite3_bind_int64(statement, 2, item_index); + sqlite3_bind_int64(statement, 3, (sqlite3_int64)image_id); + result = step_done(database, statement, "retain selected representation"); + if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1) + result = LARDON3D_PROJECT_DB_CONSTRAINT; + } + if (result == LARDON3D_PROJECT_DB_OK) + result = prepare(database, + "UPDATE selected_executions SET next_item_index=?3,stage=CASE WHEN ?3=item_count THEN 2 " + "ELSE 1 END WHERE execution_id=?1 AND stage IN(1,2) AND " + "(next_item_index=?2 OR next_item_index=?3) AND item_count>=?3", &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_int64(statement, 1, (sqlite3_int64)execution_id); + sqlite3_bind_int64(statement, 2, item_index); + sqlite3_bind_int64(statement, 3, next_item_index); + result = step_done(database, statement, "advance representation cursor"); + if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1) + result = LARDON3D_PROJECT_DB_CONSTRAINT; + } + if (result == LARDON3D_PROJECT_DB_OK) result = execute(database, "COMMIT", "commit representation"); + else (void)execute(database, "ROLLBACK", "rollback representation"); + (void)pthread_mutex_unlock(&database->mutex); + return result; +} + +Lardon3DProjectDbResult lardon3d_project_db_assign_selected_calibration_scope( + Lardon3DProjectDb *database, uint64_t execution_id, + uint64_t calibration_scope_id) { + if (!database || !valid_task_id(execution_id) || !valid_task_id(calibration_scope_id)) + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + (void)pthread_mutex_lock(&database->mutex); + sqlite3_stmt *statement = NULL; + Lardon3DProjectDbResult result = prepare(database, + "UPDATE selected_executions SET calibration_scope_id=?2,stage=3 WHERE execution_id=?1 " + "AND next_item_index=item_count AND (stage=2 OR (stage=3 AND calibration_scope_id=?2)) " + "AND NOT EXISTS(SELECT 1 FROM selected_execution_items i WHERE i.execution_id=?1 AND " + "(i.image_id IS NULL OR NOT EXISTS(SELECT 1 FROM sparse_calibration_scope_images s " + "WHERE s.scope_id=?2 AND s.image_id=i.image_id)))", &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_int64(statement, 1, (sqlite3_int64)execution_id); + sqlite3_bind_int64(statement, 2, (sqlite3_int64)calibration_scope_id); + result = step_done(database, statement, "assign selected calibration scope"); + if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1) + result = LARDON3D_PROJECT_DB_CONSTRAINT; + } + (void)pthread_mutex_unlock(&database->mutex); + return result; +} + static bool valid_catalog_id(uint64_t value) { return value > 0 && value <= INT64_MAX; } static bool valid_original_name(const char *name) { @@ -3305,6 +3856,50 @@ Lardon3DProjectDbResult lardon3d_project_db_register_image_asset( return result; } +Lardon3DProjectDbResult lardon3d_project_db_load_image_asset( + Lardon3DProjectDb *database, uint64_t asset_id, Lardon3DProjectDbImageAsset *asset) { + if (!database || !valid_catalog_id(asset_id) || !asset) + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + memset(asset, 0, sizeof(*asset)); + (void)pthread_mutex_lock(&database->mutex); + sqlite3_stmt *statement = NULL; + Lardon3DProjectDbResult result = prepare(database, + "SELECT asset_id,sha256,path,size_bytes,state,created_at FROM image_assets " + "WHERE asset_id=?1", &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_int64(statement, 1, (sqlite3_int64)asset_id); + int code = sqlite3_step(statement); + if (code == SQLITE_DONE) result = LARDON3D_PROJECT_DB_NOT_FOUND; + else if (code != SQLITE_ROW) result = sqlite_result(database, code, "load image asset"); + else { + sqlite3_int64 stored_id = sqlite3_column_int64(statement, 0); + sqlite3_int64 stored_size = sqlite3_column_int64(statement, 3); + sqlite3_int64 stored_state = sqlite3_column_int64(statement, 4); + sqlite3_int64 stored_created = sqlite3_column_int64(statement, 5); + const void *stored_hash = sqlite3_column_blob(statement, 1); + if (sqlite3_column_type(statement, 0) != SQLITE_INTEGER || stored_id <= 0 || + sqlite3_column_type(statement, 1) != SQLITE_BLOB || !stored_hash || + sqlite3_column_bytes(statement, 1) != LARDON3D_PROJECT_DB_SHA256_SIZE || + !copy_column(statement, 2, asset->path, sizeof(asset->path)) || + !valid_relative_asset_path(asset->path) || stored_size < 0 || + stored_state != LARDON3D_DB_IMAGE_ASSET_READY || stored_created < 0) { + result = LARDON3D_PROJECT_DB_CORRUPT; + } else { + asset->asset_id = (uint64_t)stored_id; + memcpy(asset->sha256, stored_hash, sizeof(asset->sha256)); + asset->size_bytes = (uint64_t)stored_size; + asset->state = LARDON3D_DB_IMAGE_ASSET_READY; + asset->created_at = stored_created; + if (!canonical_asset_path(asset->sha256, asset->path)) + result = LARDON3D_PROJECT_DB_CORRUPT; + } + } + sqlite3_finalize(statement); + } + (void)pthread_mutex_unlock(&database->mutex); + return result; +} + Lardon3DProjectDbResult lardon3d_project_db_register_image( Lardon3DProjectDb *database, uint64_t scanset_id, const unsigned char sha256[LARDON3D_PROJECT_DB_SHA256_SIZE], const char *asset_path, @@ -3785,6 +4380,10 @@ static bool valid_capture_asset_role(Lardon3DProjectDbCaptureAssetRole role) { return role == LARDON3D_DB_CAPTURE_ASSET_SOURCE || role == LARDON3D_DB_CAPTURE_ASSET_DERIVED; } +static bool valid_capture_source_kind(Lardon3DProjectDbCaptureSourceKind kind) { + return kind == LARDON3D_DB_CAPTURE_SOURCE_JPEG || kind == LARDON3D_DB_CAPTURE_SOURCE_RAW; +} + static bool valid_asset_derivation_kind(Lardon3DProjectDbAssetDerivationKind kind) { return kind == LARDON3D_DB_ASSET_DERIVATION_GENERIC_VERSIONED; } @@ -3936,6 +4535,109 @@ Lardon3DProjectDbResult lardon3d_project_db_attach_capture_source_asset( return LARDON3D_PROJECT_DB_CONSTRAINT; } +Lardon3DProjectDbResult lardon3d_project_db_record_capture_source_asset( + Lardon3DProjectDb *database, uint64_t capture_id, uint64_t asset_id, + Lardon3DProjectDbCaptureSourceKind source_kind) { + if (!database || !valid_catalog_id(capture_id) || !valid_catalog_id(asset_id) || + !valid_capture_source_kind(source_kind)) + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + + (void)pthread_mutex_lock(&database->mutex); + Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin source provenance"); + sqlite3_stmt *statement = NULL; + if (result == LARDON3D_PROJECT_DB_OK) + result = prepare(database, + "INSERT INTO capture_assets(capture_id,asset_id,role) VALUES(?1,?2,1) " + "ON CONFLICT(capture_id,asset_id) DO NOTHING", &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_int64(statement, 1, (sqlite3_int64)capture_id); + sqlite3_bind_int64(statement, 2, (sqlite3_int64)asset_id); + result = step_done(database, statement, "attach explicit source asset"); + } + if (result == LARDON3D_PROJECT_DB_OK) + result = prepare(database, + "INSERT INTO capture_source_assets(capture_id,asset_id,source_kind) " + "SELECT ?1,?2,?3 WHERE EXISTS(SELECT 1 FROM capture_assets " + "WHERE capture_id=?1 AND asset_id=?2 AND role=1) " + "ON CONFLICT(capture_id,asset_id) DO NOTHING", &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_int64(statement, 1, (sqlite3_int64)capture_id); + sqlite3_bind_int64(statement, 2, (sqlite3_int64)asset_id); + sqlite3_bind_int(statement, 3, (int)source_kind); + result = step_done(database, statement, "retain explicit source kind"); + } + if (result == LARDON3D_PROJECT_DB_OK) + result = prepare(database, + "SELECT ca.role,cs.source_kind FROM capture_assets ca LEFT JOIN capture_source_assets cs " + "ON cs.capture_id=ca.capture_id AND cs.asset_id=ca.asset_id " + "WHERE ca.capture_id=?1 AND ca.asset_id=?2", &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_int64(statement, 1, (sqlite3_int64)capture_id); + sqlite3_bind_int64(statement, 2, (sqlite3_int64)asset_id); + int code = sqlite3_step(statement); + if (code != SQLITE_ROW) { + result = code == SQLITE_DONE ? LARDON3D_PROJECT_DB_CONSTRAINT + : sqlite_result(database, code, "verify source provenance"); + } else if (sqlite3_column_type(statement, 0) != SQLITE_INTEGER || + sqlite3_column_type(statement, 1) != SQLITE_INTEGER || + sqlite3_column_int(statement, 0) != LARDON3D_DB_CAPTURE_ASSET_SOURCE || + sqlite3_column_int(statement, 1) != (int)source_kind) { + result = LARDON3D_PROJECT_DB_CONSTRAINT; + } + sqlite3_finalize(statement); + statement = NULL; + } + if (result == LARDON3D_PROJECT_DB_OK) + result = execute(database, "COMMIT", "commit source provenance"); + else + (void)execute(database, "ROLLBACK", "rollback source provenance"); + (void)pthread_mutex_unlock(&database->mutex); + return result; +} + +Lardon3DProjectDbResult lardon3d_project_db_list_capture_source_assets( + Lardon3DProjectDb *database, uint64_t capture_id, uint64_t after_asset_id, + Lardon3DProjectDbCaptureSourceAsset *assets, size_t capacity, size_t *count) { + if (count) *count = 0; + if (!database || !valid_catalog_id(capture_id) || after_asset_id > INT64_MAX || !assets || + !count || capacity == 0 || capacity > LARDON3D_PROJECT_DB_CATALOG_PAGE_MAX) + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + + (void)pthread_mutex_lock(&database->mutex); + sqlite3_stmt *statement = NULL; + Lardon3DProjectDbResult result = prepare(database, + "SELECT capture_id,asset_id,source_kind FROM capture_source_assets WHERE capture_id=?1 " + "AND asset_id>?2 ORDER BY asset_id LIMIT ?3", &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_int64(statement, 1, (sqlite3_int64)capture_id); + sqlite3_bind_int64(statement, 2, (sqlite3_int64)after_asset_id); + sqlite3_bind_int64(statement, 3, (sqlite3_int64)capacity); + int code = SQLITE_DONE; + while (*count < capacity && (code = sqlite3_step(statement)) == SQLITE_ROW) { + sqlite3_int64 stored_capture = sqlite3_column_int64(statement, 0); + sqlite3_int64 stored_asset = sqlite3_column_int64(statement, 1); + sqlite3_int64 stored_kind = sqlite3_column_int64(statement, 2); + if (sqlite3_column_type(statement, 0) != SQLITE_INTEGER || + sqlite3_column_type(statement, 1) != SQLITE_INTEGER || + sqlite3_column_type(statement, 2) != SQLITE_INTEGER || stored_capture <= 0 || + stored_asset <= 0 || + !valid_capture_source_kind((Lardon3DProjectDbCaptureSourceKind)stored_kind)) { + result = LARDON3D_PROJECT_DB_CORRUPT; + break; + } + assets[*count] = (Lardon3DProjectDbCaptureSourceAsset){ + .capture_id = (uint64_t)stored_capture, .asset_id = (uint64_t)stored_asset, + .source_kind = (Lardon3DProjectDbCaptureSourceKind)stored_kind}; + ++*count; + } + if (result == LARDON3D_PROJECT_DB_OK && *count < capacity && code != SQLITE_DONE) + result = sqlite_result(database, code, "list capture source provenance"); + sqlite3_finalize(statement); + } + (void)pthread_mutex_unlock(&database->mutex); + return result; +} + Lardon3DProjectDbResult lardon3d_project_db_list_capture_assets( Lardon3DProjectDb *database, uint64_t capture_id, uint64_t after_asset_id, Lardon3DProjectDbCaptureAsset *assets, size_t capacity, size_t *count) { @@ -5365,7 +6067,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_visual_index_update_task( return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; } return record_task_internal(db, snapshot, kind, version, checkpoint, NULL, 0, NULL, - NULL, parameters, NULL, NULL, NULL, NULL, NULL, NULL, updated_at); + NULL, parameters, NULL, NULL, NULL, NULL, NULL, NULL, NULL, updated_at); } Lardon3DProjectDbResult lardon3d_project_db_load_visual_index_update_task( @@ -5412,7 +6114,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_candidate_pair_generate_task( return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; } return record_task_internal(db, snapshot, kind, version, checkpoint, NULL, 0, NULL, - NULL, NULL, parameters, NULL, NULL, NULL, NULL, NULL, updated_at); + NULL, NULL, parameters, NULL, NULL, NULL, NULL, NULL, NULL, updated_at); } Lardon3DProjectDbResult lardon3d_project_db_load_candidate_pair_generate_task( @@ -5468,7 +6170,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_matcher_task( return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; } return record_task_internal(db, snapshot, kind, version, checkpoint, NULL, 0, NULL, - NULL, NULL, NULL, parameters, NULL, NULL, NULL, NULL, updated_at); + NULL, NULL, NULL, parameters, NULL, NULL, NULL, NULL, NULL, updated_at); } Lardon3DProjectDbResult lardon3d_project_db_load_matcher_task( @@ -5526,7 +6228,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_geometric_verifier_task( return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; } return record_task_internal(db, snapshot, kind, version, checkpoint, NULL, 0, NULL, - NULL, NULL, NULL, NULL, parameters, NULL, NULL, NULL, updated_at); + NULL, NULL, NULL, NULL, parameters, NULL, NULL, NULL, NULL, updated_at); } Lardon3DProjectDbResult lardon3d_project_db_load_geometric_verifier_task( @@ -5599,7 +6301,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_track_builder_task( } Lardon3DProjectDbResult result = record_task_internal( db, snapshot, kind, version, checkpoint, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, - NULL, NULL, updated_at); + NULL, NULL, NULL, updated_at); if (result != LARDON3D_PROJECT_DB_OK) return result; (void)pthread_mutex_lock(&db->mutex); result = execute(db, "BEGIN IMMEDIATE", "begin track builder task"); @@ -5710,7 +6412,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_sparse_sfm_task( const Lardon3DProjectDbSparseSfmTask *parameters, int64_t updated_at) { if (!parameters) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; return record_task_internal(database, snapshot, kind, version, checkpoint, NULL, 0, NULL, NULL, - NULL, NULL, NULL, NULL, parameters, NULL, NULL, updated_at); + NULL, NULL, NULL, NULL, parameters, NULL, NULL, NULL, updated_at); } static bool read_u64_blob(sqlite3_stmt *statement, int column, uint64_t *value) { diff --git a/src/raw_development.cpp b/src/raw_development.cpp index b2c1903..79debaa 100644 --- a/src/raw_development.cpp +++ b/src/raw_development.cpp @@ -336,3 +336,47 @@ extern "C" Lardon3DRawDevelopmentResult lardon3d_raw_develop_to_capture( } catch (const std::bad_alloc &) { return LARDON3D_RAW_DEVELOPMENT_OUT_OF_MEMORY; } catch (...) { return LARDON3D_RAW_DEVELOPMENT_INTERNAL_ERROR; } } + +extern "C" Lardon3DRawDevelopmentResult lardon3d_raw_develop_asset_to_capture( + Lardon3DAppState *state, uint64_t capture_id, uint64_t source_asset_id, + uint64_t producer_task_id, int64_t created_at, Lardon3DRawDevelopmentOutput *output) { + if (!state || !state->project_loaded || !state->project_db || capture_id == 0 || + source_asset_id == 0 || created_at < 0 || !output) + return LARDON3D_RAW_DEVELOPMENT_INVALID_ARGUMENT; + std::memset(output, 0, sizeof(*output)); + try { + bool explicit_raw = false; + uint64_t after_asset_id = source_asset_id - 1u; + Lardon3DProjectDbCaptureSourceAsset relation{}; + size_t count = 0; + Lardon3DProjectDbResult listed = lardon3d_project_db_list_capture_source_assets( + state->project_db, capture_id, after_asset_id, &relation, 1, &count); + if (listed != LARDON3D_PROJECT_DB_OK) return db_result(listed); + explicit_raw = count == 1 && relation.asset_id == source_asset_id && + relation.source_kind == LARDON3D_DB_CAPTURE_SOURCE_RAW; + if (!explicit_raw) return LARDON3D_RAW_DEVELOPMENT_CONSTRAINT; + + Lardon3DProjectDbImageAsset asset{}; + Lardon3DProjectDbResult loaded = lardon3d_project_db_load_image_asset( + state->project_db, source_asset_id, &asset); + if (loaded != LARDON3D_PROJECT_DB_OK) return db_result(loaded); + char managed_source[LARDON3D_APP_STATE_PATH_CAPACITY]; + if (!asset_file_path(state, asset, managed_source)) return LARDON3D_RAW_DEVELOPMENT_IO_ERROR; + if (!verify_managed_asset(managed_source, asset.sha256)) + return LARDON3D_RAW_DEVELOPMENT_SOURCE_CHANGED; + + // The pathname passed to the frozen developer is resolved only after the + // explicit durable ID and RAW relation are validated. Republishing these + // immutable managed bytes must converge to that same asset ID. + Lardon3DRawDevelopmentResult result = lardon3d_raw_develop_to_capture( + state, capture_id, managed_source, producer_task_id, created_at, output); + if (result != LARDON3D_RAW_DEVELOPMENT_OK) return result; + return output->source_asset.asset_id == source_asset_id + ? LARDON3D_RAW_DEVELOPMENT_OK + : LARDON3D_RAW_DEVELOPMENT_CONSTRAINT; + } catch (const std::bad_alloc &) { + return LARDON3D_RAW_DEVELOPMENT_OUT_OF_MEMORY; + } catch (...) { + return LARDON3D_RAW_DEVELOPMENT_INTERNAL_ERROR; + } +} diff --git a/src/raw_development_task.cpp b/src/raw_development_task.cpp new file mode 100644 index 0000000..935a534 --- /dev/null +++ b/src/raw_development_task.cpp @@ -0,0 +1,197 @@ +extern "C" { +#include +#include +#include +#include +} + +#include +#include + +namespace { +/* One admitted RAW execution owns a conservative allowance for the bounded + * 40 MP decoder image, LibRaw storage, BGR/PNG buffers, validation decode, and + * immutable-asset publication. This is an operational hardware bound, not a + * scientific limit on the number of Captures. The Queue/Governor reservation + * exists only for this one Task callback and is released by Task runtime on + * every terminal path. */ +constexpr uint64_t kRawWorkingBytes = UINT64_C(2) * 1024u * 1024u * 1024u; + +struct Context { + char project_path[LARDON3D_APP_STATE_PATH_CAPACITY]{}; + Lardon3DProjectDb *database{}; + Lardon3DResourceGovernor *governor{}; + uint64_t capture_id{}; + uint64_t source_asset_id{}; +}; + +void destroy(void *value) { delete static_cast(value); } + +void runtime(const Context *context, Lardon3DAppState *state) { + lardon3d_app_state_init(state); + state->project_loaded = true; + state->project_db = context->database; + state->resource_governor = context->governor; + std::snprintf(state->project_path, sizeof(state->project_path), "%s", context->project_path); +} + +bool checkpoint(Context *context, Lardon3DTask *task, + Lardon3DProjectDbRawDevelopmentTaskPhase phase, uint64_t image_id) { + Lardon3DAppState state; + runtime(context, &state); + Lardon3DProjectDbRawDevelopmentTask parameters{ + lardon3d_task_id(task), context->capture_id, context->source_asset_id, phase, + image_id != 0, image_id}; + return lardon3d_project_checkpoint_raw_development_task(&state, task, ¶meters) == + LARDON3D_PROJECT_TASK_CHECKPOINT_OK; +} + +bool run_impl(Lardon3DTask *task, Context *context) { + Lardon3DProjectDbRawDevelopmentTask persisted{}; + if (lardon3d_project_db_load_raw_development_task( + context->database, lardon3d_task_id(task), &persisted) != LARDON3D_PROJECT_DB_OK) + return lardon3d_task_fail(task, "État durable du développement RAW invalide."); + if (!lardon3d_task_checkpoint(task)) return false; + if (persisted.phase == LARDON3D_RAW_DEVELOPMENT_TASK_PUBLISHED) { + return lardon3d_task_set_progress(task, 100, "Développement RAW déjà publié.") && + checkpoint(context, task, LARDON3D_RAW_DEVELOPMENT_TASK_PUBLISHED, + persisted.image_id); + } + + Lardon3DProjectDbCapture capture{}; + if (lardon3d_project_db_load_capture(context->database, context->capture_id, &capture) != + LARDON3D_PROJECT_DB_OK) + return lardon3d_task_fail(task, "Capture du développement RAW introuvable."); + Lardon3DAppState state; + runtime(context, &state); + Lardon3DRawDevelopmentOutput output{}; + Lardon3DRawDevelopmentResult developed = lardon3d_raw_develop_asset_to_capture( + &state, context->capture_id, context->source_asset_id, lardon3d_task_id(task), + capture.created_at, &output); + if (developed != LARDON3D_RAW_DEVELOPMENT_OK) + return lardon3d_task_fail(task, "Développement RAW S3-B1 impossible."); + + /* Immutable output publication may precede this checkpoint if the process + * dies. The frozen developer is content-addressed and exact retry converges; + * once this atomic typed/generic checkpoint succeeds, recovery never guesses + * which image was published. */ + if (!lardon3d_task_set_progress(task, 100, "Représentation RAW publiée.") || + !checkpoint(context, task, LARDON3D_RAW_DEVELOPMENT_TASK_PUBLISHED, + output.image.image_id)) + return lardon3d_task_fail(task, "Checkpoint du développement RAW impossible."); + return true; +} + +bool run(Lardon3DTask *task, void *value) noexcept { + try { + return run_impl(task, static_cast(value)); + } catch (const std::bad_alloc &) { + return lardon3d_task_fail(task, "Mémoire insuffisante pour le développement RAW."); + } catch (...) { + return lardon3d_task_fail(task, "Erreur interne du développement RAW."); + } +} + +void finished(const Lardon3DTask *task, void *value) noexcept { + try { + Lardon3DAppState state; + runtime(static_cast(value), &state); + (void)lardon3d_project_checkpoint_task(&state, task); + } catch (...) { + } +} + +Context *make_context(const Lardon3DTaskReconstructionContext *runtime_context, + uint64_t capture_id, uint64_t source_asset_id) { + if (!runtime_context || !runtime_context->project_path || + !runtime_context->project_path[0] || !runtime_context->project_db || + !runtime_context->resource_governor) + return nullptr; + Context *context = new (std::nothrow) Context; + if (!context) return nullptr; + int written = std::snprintf(context->project_path, sizeof(context->project_path), "%s", + runtime_context->project_path); + if (written <= 0 || written >= static_cast(sizeof(context->project_path))) { + delete context; + return nullptr; + } + context->database = runtime_context->project_db; + context->governor = runtime_context->resource_governor; + context->capture_id = capture_id; + context->source_asset_id = source_asset_id; + return context; +} +} // namespace + +extern "C" bool lardon3d_raw_development_task_reconstruct( + const Lardon3DTaskDurableSnapshot *snapshot, void *value, + Lardon3DTaskKindBinding *binding) { + try { + auto *runtime_context = static_cast(value); + if (!snapshot || !runtime_context || !binding) return false; + Lardon3DProjectDbRawDevelopmentTask persisted{}; + if (lardon3d_project_db_load_raw_development_task( + runtime_context->project_db, snapshot->id, &persisted) != LARDON3D_PROJECT_DB_OK) + return false; + Context *context = make_context(runtime_context, persisted.capture_id, + persisted.source_asset_id); + if (!context) return false; + *binding = {run, context, destroy, finished, context}; + return true; + } catch (...) { + return false; + } +} + +extern "C" Lardon3DTask *lardon3d_project_create_raw_development_task( + Lardon3DAppState *state, uint64_t capture_id, uint64_t source_asset_id, + uint64_t *task_id) { + if (task_id) *task_id = 0; + try { + if (!state || !state->project_loaded || !state->project_db || + !state->resource_governor || !task_id || capture_id == 0 || source_asset_id == 0) + return nullptr; + Lardon3DTaskReconstructionContext runtime_context{ + state->project_path, state->project_db, state->resource_governor, nullptr}; + Context *context = make_context(&runtime_context, capture_id, source_asset_id); + if (!context) return nullptr; + uint64_t id = 0; + if (lardon3d_project_db_allocate_task_id(state->project_db, &id) != LARDON3D_PROJECT_DB_OK) { + delete context; + return nullptr; + } + Lardon3DResourceEstimate estimate{kRawWorkingBytes + sizeof(Context), 0, 0, 0, + 1, 1, 1, 0, 1, LARDON3D_RESOURCE_TASK_MIXED}; + Lardon3DTask *task = lardon3d_task_create_typed( + "Développement RAW S3-B1", &estimate, LARDON3D_RAW_DEVELOPMENT_TASK_KIND, + LARDON3D_RAW_DEVELOPMENT_TASK_KIND_VERSION, run, context, destroy); + if (!task || !lardon3d_task_assign_id(task, id) || + !lardon3d_task_set_finished_callback(task, finished, context) || + !checkpoint(context, task, LARDON3D_RAW_DEVELOPMENT_TASK_PENDING, 0)) { + lardon3d_task_destroy(task); + return nullptr; + } + *task_id = id; + return task; + } catch (...) { + return nullptr; + } +} + +extern "C" bool lardon3d_project_enqueue_raw_development( + Lardon3DAppState *state, uint64_t capture_id, uint64_t source_asset_id, + uint64_t *task_id) { + try { + if (!state || !state->task_queue) return false; + Lardon3DTask *task = lardon3d_project_create_raw_development_task( + state, capture_id, source_asset_id, task_id); + if (!task) return false; + if (!lardon3d_task_queue_add(state->task_queue, task, nullptr)) { + lardon3d_task_destroy(task); + return false; + } + return true; + } catch (...) { + return false; + } +} diff --git a/src/task_kinds.c b/src/task_kinds.c index 9f7212f..ec4bdb5 100644 --- a/src/task_kinds.c +++ b/src/task_kinds.c @@ -6,6 +6,9 @@ #ifdef LARDON3D_PHOTO_QUALITY_TASK_AVAILABLE #include #endif +#ifdef LARDON3D_RAW_DEVELOPMENT_TASK_AVAILABLE +#include +#endif #include #include #include @@ -24,6 +27,13 @@ const Lardon3DTaskKindRegistry *lardon3d_task_kind_registry_production(void) { static const Lardon3DTaskKindDescriptor descriptors[] = { +#ifdef LARDON3D_RAW_DEVELOPMENT_TASK_AVAILABLE + { + .kind = LARDON3D_RAW_DEVELOPMENT_TASK_KIND, + .kind_version = LARDON3D_RAW_DEVELOPMENT_TASK_KIND_VERSION, + .reconstruct = lardon3d_raw_development_task_reconstruct, + }, +#endif #ifdef LARDON3D_PHOTO_QUALITY_TASK_AVAILABLE { .kind = LARDON3D_PHOTO_QUALITY_TASK_KIND, diff --git a/tests/test_acquisition_ingest.cpp b/tests/test_acquisition_ingest.cpp index ccf9fd1..3e26f45 100644 --- a/tests/test_acquisition_ingest.cpp +++ b/tests/test_acquisition_ingest.cpp @@ -146,6 +146,24 @@ bool capture_assets(Lardon3DProjectDb *database, uint64_t capture_id, size_t exp return true; } +bool capture_source_kinds(Lardon3DProjectDb *database, uint64_t capture_id, + size_t expected_jpeg, size_t expected_raw) { + Lardon3DProjectDbCaptureSourceAsset assets[64]{}; + size_t count = 0u; + if (lardon3d_project_db_list_capture_source_assets(database, capture_id, 0, assets, 64, + &count) != LARDON3D_PROJECT_DB_OK || + count != expected_jpeg + expected_raw) + return false; + size_t jpeg = 0u, raw = 0u; + for (size_t index = 0; index < count; ++index) { + if (assets[index].capture_id != capture_id || assets[index].asset_id == 0) return false; + if (assets[index].source_kind == LARDON3D_DB_CAPTURE_SOURCE_JPEG) ++jpeg; + else if (assets[index].source_kind == LARDON3D_DB_CAPTURE_SOURCE_RAW) ++raw; + else return false; + } + return jpeg == expected_jpeg && raw == expected_raw; +} + bool test_ingest_grouping_and_resume() { Fixture fixture; CHECK(fixture.open()); @@ -181,7 +199,8 @@ bool test_ingest_grouping_and_resume() { &jpeg_options, &paired) == LARDON3D_ACQUISITION_INGEST_OK); CHECK(paired.group_count == 1 && paired.groups[0].basis == LARDON3D_ACQUISITION_GROUP_STRONG && paired.groups[0].source_count == 2 && capture_count(strong.database, strong.scanset.scanset_id) == 1 && - capture_assets(strong.database, paired.groups[0].capture_id, 2)); + capture_assets(strong.database, paired.groups[0].capture_id, 2) && + capture_source_kinds(strong.database, paired.groups[0].capture_id, 1, 1)); /* A competing equal strong candidate remains entirely ungrouped. */ Fixture ambiguous; @@ -215,6 +234,7 @@ bool test_ingest_grouping_and_resume() { CHECK(lardon3d_acquisition_ingest(&resumed.state, resumed.scanset.scanset_id, one, 1, &resume_options, &first) == LARDON3D_ACQUISITION_INGEST_OK && first.groups[0].capture_id == existing.capture_id); Lardon3DAcquisitionIngestOutput retry{}; CHECK(lardon3d_acquisition_ingest(&resumed.state, resumed.scanset.scanset_id, one, 1, &resume_options, &retry) == LARDON3D_ACQUISITION_INGEST_OK && capture_count(resumed.database, resumed.scanset.scanset_id) == 1 && capture_assets(resumed.database, existing.capture_id, 1)); + CHECK(capture_source_kinds(resumed.database, existing.capture_id, 1, 0)); uint64_t images = 0; CHECK(lardon3d_project_db_count_images(resumed.database, resumed.scanset.scanset_id, &images) == LARDON3D_PROJECT_DB_OK && images == 1); fixture.close(); strong.close(); ambiguous.close(); resumed.close(); @@ -245,6 +265,16 @@ bool test_explicit_and_db_helper() { Lardon3DProjectDbImageRegisterStatus status{}; Lardon3DProjectDbImage image{}; CHECK(lardon3d_project_db_publish_source_capture_image(fixture.database, capture.capture_id, asset.asset_id, "helper.JPG", helper_path, 0, 8, false, &status, &image) == LARDON3D_PROJECT_DB_OK && status == LARDON3D_PROJECT_DB_IMAGE_REGISTERED); + CHECK(lardon3d_project_db_record_capture_source_asset( + fixture.database, capture.capture_id, asset.asset_id, + LARDON3D_DB_CAPTURE_SOURCE_JPEG) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_record_capture_source_asset( + fixture.database, capture.capture_id, asset.asset_id, + LARDON3D_DB_CAPTURE_SOURCE_JPEG) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_record_capture_source_asset( + fixture.database, capture.capture_id, asset.asset_id, + LARDON3D_DB_CAPTURE_SOURCE_RAW) == LARDON3D_PROJECT_DB_CONSTRAINT); + CHECK(capture_source_kinds(fixture.database, capture.capture_id, 1, 0)); uint64_t selected = 0; CHECK(lardon3d_project_db_get_selected_capture_image(fixture.database, capture.capture_id, &selected) == LARDON3D_PROJECT_DB_NOT_FOUND); CHECK(lardon3d_project_db_publish_source_capture_image(fixture.database, capture.capture_id, asset.asset_id, diff --git a/tests/test_calibration_bootstrap.c b/tests/test_calibration_bootstrap.c new file mode 100644 index 0000000..f0ddc22 --- /dev/null +++ b/tests/test_calibration_bootstrap.c @@ -0,0 +1,167 @@ +#include +#include +#include +#include +#include +#include +#include + +#include + +#define CHECK(condition) do { if (!(condition)) { \ + fprintf(stderr, "calibration bootstrap failure at line %d: %s\n", __LINE__, #condition); \ + return false; } } while (0) + +enum { ARTIFACT_SIZE = 292 }; + +static bool sql(const char *path, const char *text) { + sqlite3 *connection = NULL; + if (sqlite3_open(path, &connection) != SQLITE_OK) return false; + int code = sqlite3_exec(connection, text, NULL, NULL, NULL); + return sqlite3_close(connection) == SQLITE_OK && code == SQLITE_OK; +} + +static void put_u32(unsigned char *bytes, size_t *offset, uint32_t value) { + for (size_t index = 0; index < 4; ++index) bytes[(*offset)++] = (unsigned char)(value >> (8 * index)); +} + +static void put_u64(unsigned char *bytes, size_t *offset, uint64_t value) { + for (size_t index = 0; index < 8; ++index) bytes[(*offset)++] = (unsigned char)(value >> (8 * index)); +} + +static void put_f64(unsigned char *bytes, size_t *offset, double value) { + uint64_t bits; + memcpy(&bits, &value, sizeof(bits)); + put_u64(bytes, offset, bits); +} + +static bool digest(const unsigned char *bytes, size_t size, unsigned char output[32]) { + unsigned int output_size = 0; + return EVP_Digest(bytes, size, output, &output_size, EVP_sha256(), NULL) == 1 && + output_size == 32; +} + +static void artifact(unsigned char bytes[ARTIFACT_SIZE]) { + memset(bytes, 0, ARTIFACT_SIZE); + size_t offset = 0; + memcpy(bytes + offset, "L3DCALB1", 8); offset += 8; + put_u32(bytes, &offset, 1); put_u32(bytes, &offset, 1); + put_u32(bytes, &offset, 1); put_u32(bytes, &offset, 1); + /* Four distinct nonzero evidence digests stand for executable, + * configuration, initialization and validation evidence. */ + for (unsigned int evidence = 1; evidence <= 4; ++evidence) { + memset(bytes + offset, (int)evidence, 32); + offset += 32; + } + put_u64(bytes, &offset, 1); + memset(bytes + offset, 0x11, 32); offset += 32; + put_u32(bytes, &offset, 4000); put_u32(bytes, &offset, 2250); + put_f64(bytes, &offset, 3072.6); put_f64(bytes, &offset, 3053.54); + put_f64(bytes, &offset, 1969.56); put_f64(bytes, &offset, 1118.52); + put_f64(bytes, &offset, 0.0452928); put_f64(bytes, &offset, -0.0377292); + put_f64(bytes, &offset, 0.00479862); put_f64(bytes, &offset, 0.00672575); + put_u32(bytes, &offset, 56); put_u32(bytes, &offset, 1000); + put_f64(bytes, &offset, 0.4); put_f64(bytes, &offset, 0.000001); + put_u32(bytes, &offset, 15); +} + +static bool run(void) { + char directory[] = "/tmp/lardon3d-calibration-bootstrap-XXXXXX"; + CHECK(mkdtemp(directory) != NULL); + char path[512]; + CHECK(snprintf(path, sizeof(path), "%s/project.db", directory) > 0); + Lardon3DProjectDb *database = NULL; + char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; + CHECK(lardon3d_project_db_open(path, &database, error) == LARDON3D_PROJECT_DB_OK); + lardon3d_project_db_close(database); + /* This fixture establishes the already-frozen operational mappings. The + * importer may validate them but must never infer them from equal IDs. */ + CHECK(sql(path, + "INSERT INTO scansets(scanset_id,name,created_at,updated_at) VALUES(1,'scope',1,1);" + "INSERT INTO tasks VALUES(1,'quality','photo_quality.triage',1,5,5,100,1,0,0,0,0,1);" + "INSERT INTO tasks VALUES(2,'campaign','acquisition_campaign.run',1,5,5,100,1,0,0,0,0,1);" + "INSERT INTO photo_quality_triage_tasks VALUES(1,1,2,1,X'01');" + "INSERT INTO photo_quality_triage_results VALUES(1,1,0,1,0,1,0,100,100,100,100," + "1.0,1.0,0.0,0.0,1.0,1.0,0.0,'GOOD');" + "INSERT INTO acquisition_campaign_tasks VALUES(2,1,2,2,X'02');" + "INSERT INTO captures(capture_id,scanset_id,created_at) VALUES(1,1,1);" + "INSERT INTO acquisition_campaign_captures VALUES(2,2,1);" + "INSERT INTO image_assets(asset_id,sha256,path,size_bytes,state,created_at) VALUES" + "(1,X'1111111111111111111111111111111111111111111111111111111111111111'," + "'assets/images/11/1111111111111111111111111111111111111111111111111111111111111111'," + "1,1,1);" + "INSERT INTO images(image_id,scanset_id,asset_id,original_name,source_path,imported_at) " + "VALUES(1,1,1,'selected.jpg','/source/selected.jpg',1);" + "INSERT INTO capture_images VALUES(1,1);")); + CHECK(lardon3d_project_db_open(path, &database, error) == LARDON3D_PROJECT_DB_OK); + Lardon3DProjectDbSelectedExecutionItem item = { + .item_index = 0, + .quality_group_id = 1, + .campaign_group_id = 2, + .capture_id = 1, + .representation_source = LARDON3D_SELECTED_REPRESENTATION_SOURCE_IMAGE}; + Lardon3DProjectDbSelectedExecution execution; + CHECK(lardon3d_project_db_create_selected_execution(database, 1, 2, &item, 1, 10, + &execution) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_record_selected_representation(database, execution.execution_id, + 0, 1, 1) == LARDON3D_PROJECT_DB_OK); + + unsigned char bytes[ARTIFACT_SIZE], hash[32], wrong_hash[32] = {0}; + artifact(bytes); + CHECK(digest(bytes, sizeof(bytes), hash)); + Lardon3DCalibrationBootstrapOutput output; + CHECK(lardon3d_calibration_bootstrap_import(database, execution.execution_id, bytes, + sizeof(bytes), wrong_hash, &output) == + LARDON3D_CALIBRATION_BOOTSTRAP_PROVENANCE_MISMATCH); + + bytes[160] ^= 1u; + CHECK(digest(bytes, sizeof(bytes), wrong_hash)); + CHECK(lardon3d_calibration_bootstrap_import(database, execution.execution_id, bytes, + sizeof(bytes), wrong_hash, &output) == + LARDON3D_CALIBRATION_BOOTSTRAP_SELECTION_CONFLICT); + artifact(bytes); + bytes[8] = 2; + CHECK(digest(bytes, sizeof(bytes), wrong_hash)); + CHECK(lardon3d_calibration_bootstrap_import(database, execution.execution_id, bytes, + sizeof(bytes), wrong_hash, &output) == + LARDON3D_CALIBRATION_BOOTSTRAP_MALFORMED_ARTIFACT); + artifact(bytes); + bytes[ARTIFACT_SIZE - 4] = 14; + CHECK(digest(bytes, sizeof(bytes), wrong_hash)); + CHECK(lardon3d_calibration_bootstrap_import(database, execution.execution_id, bytes, + sizeof(bytes), wrong_hash, &output) == + LARDON3D_CALIBRATION_BOOTSTRAP_MALFORMED_ARTIFACT); + + artifact(bytes); + Lardon3DCalibrationBootstrapResult imported = lardon3d_calibration_bootstrap_import( + database, execution.execution_id, bytes, sizeof(bytes), hash, &output); + if (imported != LARDON3D_CALIBRATION_BOOTSTRAP_OK) + fprintf(stderr, "valid artifact import result=%d\n", (int)imported); + CHECK(imported == LARDON3D_CALIBRATION_BOOTSTRAP_OK); + CHECK(output.calibration_count == 1 && output.scope.scope_id > 0 && + memcmp(output.artifact_sha256, hash, 32) == 0); + Lardon3DSparseCalibrationMember member; + size_t count = 0; + uint64_t next = 0; + CHECK(lardon3d_sparse_calibration_scope_list_members(database, output.scope.scope_id, 0, + &member, 1, &count, &next) == LARDON3D_PROJECT_DB_OK && count == 1 && + member.image_id == 1); + Lardon3DSparseCalibration calibration; + CHECK(lardon3d_sparse_calibration_load(database, member.calibration_id, &calibration) == + LARDON3D_PROJECT_DB_OK && + calibration.provenance_kind == LARDON3D_SPARSE_SFM_PROVENANCE_IMPORTED_TRUSTED && + memcmp(calibration.provenance_fingerprint, hash, 32) == 0); + CHECK(lardon3d_calibration_bootstrap_import(database, execution.execution_id, bytes, + sizeof(bytes), hash, &output) == LARDON3D_CALIBRATION_BOOTSTRAP_OK); + CHECK(lardon3d_project_db_load_selected_execution(database, execution.execution_id, + &execution) == LARDON3D_PROJECT_DB_OK && + execution.stage == LARDON3D_SELECTED_EXECUTION_READY && + execution.calibration_scope_id == output.scope.scope_id); + + lardon3d_project_db_close(database); + CHECK(unlink(path) == 0); + CHECK(rmdir(directory) == 0); + return true; +} + +int main(void) { return run() ? EXIT_SUCCESS : EXIT_FAILURE; } diff --git a/tests/test_project_db.c b/tests/test_project_db.c index 5d7e9f5..0dafd48 100644 --- a/tests/test_project_db.c +++ b/tests/test_project_db.c @@ -116,7 +116,7 @@ static bool create_future_database(const char *path) { sqlite3_exec( connection, "CREATE TABLE metadata(key TEXT PRIMARY KEY,value INTEGER NOT NULL);" - "INSERT INTO metadata VALUES('schema_version',22);", + "INSERT INTO metadata VALUES('schema_version',23);", NULL, NULL, NULL) == SQLITE_OK; return sqlite3_close(connection) == SQLITE_OK && ok; } @@ -887,7 +887,7 @@ static bool run_test(void) { char v10_path[512], failed_v11_path[512]; char v13_path[512], true_v14_path[512], failed_v14_path[512], failed_v15_path[512], true_v15_path[512], v17_path[512], - failed_v18_path[512], failed_v19_path[512]; + failed_v18_path[512], failed_v19_path[512], raw_missing_table_path[512]; CHECK(snprintf(database_path, sizeof(database_path), "%s/project.db", directory) > 0); CHECK(snprintf(artifact_path, sizeof(artifact_path), "%s/artifact.bin", @@ -935,12 +935,14 @@ static bool run_test(void) { "%s/failed-v18.db", directory) > 0); CHECK(snprintf(failed_v19_path, sizeof(failed_v19_path), "%s/failed-v19.db", directory) > 0); + CHECK(snprintf(raw_missing_table_path, sizeof(raw_missing_table_path), + "%s/raw-missing-table.db", directory) > 0); char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; Lardon3DProjectDb *database = NULL; CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(database && lardon3d_project_db_schema_version(database) == 21); + CHECK(database && lardon3d_project_db_schema_version(database) == 22); bool legacy_pending = true; CHECK(lardon3d_project_db_legacy_catalog_pending(database, &legacy_pending) == LARDON3D_PROJECT_DB_OK && @@ -1499,7 +1501,7 @@ static bool run_test(void) { database = NULL; CHECK(query_integer(database_path, "SELECT value FROM metadata WHERE key='schema_version'", - 21)); + 22)); CHECK(query_integer(database_path, "SELECT count(*) FROM tasks WHERE task_id=1", 1)); @@ -1521,13 +1523,15 @@ static bool run_test(void) { "created_at) VALUES(1,1,1,1,1,zeroblob(32),1,2,1,0,0,1);" "DROP TABLE incremental_reconstruction_tasks;" "DROP TABLE incremental_reconstructions;" + "DROP TABLE selected_execution_items;DROP TABLE selected_executions;" + "DROP TABLE photo_quality_triage_results;DROP TABLE photo_quality_triage_tasks;" "DROP TABLE acquisition_campaign_captures;DROP TABLE acquisition_campaign_tasks;" "DROP TABLE asset_derivations;DROP TABLE capture_selections;DROP TABLE capture_assets;" "DROP TABLE capture_images;DROP TABLE captures;" "UPDATE metadata SET value=17 WHERE key='schema_version';")); CHECK(lardon3d_project_db_open(v17_path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_schema_version(database) == 21); + CHECK(lardon3d_project_db_schema_version(database) == 22); lardon3d_project_db_close(database); database = NULL; CHECK(query_integer(v17_path, @@ -1551,6 +1555,8 @@ static bool run_test(void) { database = NULL; CHECK(execute_test_sql( failed_v19_path, + "DROP TABLE selected_execution_items;DROP TABLE selected_executions;" + "DROP TABLE photo_quality_triage_results;DROP TABLE photo_quality_triage_tasks;" "DROP TABLE acquisition_campaign_captures;DROP TABLE acquisition_campaign_tasks;" "DROP TABLE asset_derivations;DROP TABLE capture_selections;DROP TABLE capture_assets;" "DROP TABLE capture_images;DROP TABLE captures;" @@ -1566,13 +1572,15 @@ static bool run_test(void) { "name='captures'", 0)); CHECK(lardon3d_project_db_open(failed_v19_path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_schema_version(database) == 21); + CHECK(lardon3d_project_db_schema_version(database) == 22); lardon3d_project_db_close(database); database = NULL; CHECK(execute_test_sql( failed_v18_path, "DROP TABLE incremental_reconstruction_tasks;" "DROP TABLE incremental_reconstructions;" + "DROP TABLE selected_execution_items;DROP TABLE selected_executions;" + "DROP TABLE photo_quality_triage_results;DROP TABLE photo_quality_triage_tasks;" "DROP TABLE acquisition_campaign_captures;DROP TABLE acquisition_campaign_tasks;" "DROP TABLE asset_derivations;DROP TABLE capture_selections;DROP TABLE capture_assets;" "DROP TABLE capture_images;DROP TABLE captures;" @@ -1618,7 +1626,7 @@ static bool run_test(void) { CHECK(create_v1_database(legacy_path)); CHECK(lardon3d_project_db_open(legacy_path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_schema_version(database) == 21); + CHECK(lardon3d_project_db_schema_version(database) == 22); CHECK(lardon3d_project_db_get_project(database, &loaded_project) == LARDON3D_PROJECT_DB_OK && strcmp(loaded_project.stable_id, "legacy-project") == 0); @@ -1633,7 +1641,17 @@ static bool run_test(void) { database = NULL; CHECK(query_integer(legacy_path, "SELECT value FROM metadata WHERE key='schema_version'", - 21)); + 22)); + CHECK(create_v1_database(raw_missing_table_path)); + CHECK(lardon3d_project_db_open(raw_missing_table_path, &database, error) == + LARDON3D_PROJECT_DB_OK); + lardon3d_project_db_close(database); + database = NULL; + CHECK(execute_test_sql(raw_missing_table_path, + "DROP TABLE raw_development_tasks;")); + CHECK(lardon3d_project_db_open(raw_missing_table_path, &database, error) == + LARDON3D_PROJECT_DB_CORRUPT); + CHECK(database == NULL); CHECK(create_v1_database(failed_migration_path)); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V2", "1", 1) == 0); @@ -1664,7 +1682,7 @@ static bool run_test(void) { lardon3d_project_db_close(database); database = NULL; CHECK(query_integer( - v2_path, "SELECT value FROM metadata WHERE key='schema_version'", 21)); + v2_path, "SELECT value FROM metadata WHERE key='schema_version'", 22)); CHECK(create_v2_database(failed_v3_migration_path)); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V3", "1", 1) == 0); @@ -1698,7 +1716,7 @@ static bool run_test(void) { lardon3d_project_db_close(database); database = NULL; CHECK(query_integer( - v3_path, "SELECT value FROM metadata WHERE key='schema_version'", 21)); + v3_path, "SELECT value FROM metadata WHERE key='schema_version'", 22)); CHECK(create_v3_database(failed_v4_path)); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V4", "1", 1) == 0); @@ -1720,7 +1738,7 @@ static bool run_test(void) { fprintf(stderr, "Migration v4 (%d): %s\n", (int)v4_result, error); } CHECK(v4_result == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_schema_version(database) == 21); + CHECK(lardon3d_project_db_schema_version(database) == 22); CHECK(lardon3d_project_db_load_task(database, 9, &task) == LARDON3D_PROJECT_DB_OK); CHECK(lardon3d_project_db_load_artifact(database, "legacy-artifact", @@ -1775,7 +1793,7 @@ static bool run_test(void) { error); } CHECK(retry_v7 == LARDON3D_PROJECT_DB_OK && - lardon3d_project_db_schema_version(database) == 21); + lardon3d_project_db_schema_version(database) == 22); lardon3d_project_db_close(database); database = NULL; @@ -1785,7 +1803,7 @@ static bool run_test(void) { 5)); CHECK(lardon3d_project_db_open(direct_v5_path, &database, error) == LARDON3D_PROJECT_DB_OK && - lardon3d_project_db_schema_version(database) == 21); + lardon3d_project_db_schema_version(database) == 22); lardon3d_project_db_close(database); database = NULL; @@ -1794,11 +1812,11 @@ static bool run_test(void) { v8_path, "SELECT value FROM metadata WHERE key='schema_version'", 7)); CHECK(lardon3d_project_db_open(v8_path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_schema_version(database) == 21); + CHECK(lardon3d_project_db_schema_version(database) == 22); lardon3d_project_db_close(database); database = NULL; CHECK(query_integer( - v8_path, "SELECT value FROM metadata WHERE key='schema_version'", 21)); + v8_path, "SELECT value FROM metadata WHERE key='schema_version'", 22)); CHECK(create_v7_database(failed_v8_path)); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V8", "1", 1) == 0); @@ -1815,7 +1833,7 @@ static bool run_test(void) { 0)); CHECK(lardon3d_project_db_open(failed_v8_path, &database, error) == LARDON3D_PROJECT_DB_OK && - lardon3d_project_db_schema_version(database) == 21); + lardon3d_project_db_schema_version(database) == 22); lardon3d_project_db_close(database); database = NULL; @@ -1834,11 +1852,11 @@ static bool run_test(void) { 0)); CHECK(lardon3d_project_db_open(v10_path, &database, error) == LARDON3D_PROJECT_DB_OK && - lardon3d_project_db_schema_version(database) == 21); + lardon3d_project_db_schema_version(database) == 22); lardon3d_project_db_close(database); database = NULL; CHECK(query_integer( - v10_path, "SELECT value FROM metadata WHERE key='schema_version'", 21)); + v10_path, "SELECT value FROM metadata WHERE key='schema_version'", 22)); CHECK( query_integer(v10_path, "SELECT count(*) FROM sqlite_master WHERE type='table' AND " @@ -1865,7 +1883,7 @@ static bool run_test(void) { 1)); CHECK(lardon3d_project_db_open(failed_v11_path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_schema_version(database) == 21); + CHECK(lardon3d_project_db_schema_version(database) == 22); lardon3d_project_db_close(database); database = NULL; @@ -1879,11 +1897,11 @@ static bool run_test(void) { 0)); CHECK(lardon3d_project_db_open(v13_path, &database, error) == LARDON3D_PROJECT_DB_OK && - lardon3d_project_db_schema_version(database) == 21); + lardon3d_project_db_schema_version(database) == 22); lardon3d_project_db_close(database); database = NULL; CHECK(query_integer( - v13_path, "SELECT value FROM metadata WHERE key='schema_version'", 21)); + v13_path, "SELECT value FROM metadata WHERE key='schema_version'", 22)); CHECK( query_integer(v13_path, "SELECT count(*) FROM sqlite_master WHERE type='table' AND " @@ -1906,7 +1924,7 @@ static bool run_test(void) { true_v14_path); } CHECK(true_v14_result == LARDON3D_PROJECT_DB_OK && - lardon3d_project_db_schema_version(database) == 21); + lardon3d_project_db_schema_version(database) == 22); lardon3d_project_db_close(database); database = NULL; CHECK( @@ -2021,7 +2039,7 @@ static bool run_test(void) { if (true_v15_result != LARDON3D_PROJECT_DB_OK) fprintf(stderr, "true v15 upgrade: %d %s\n", true_v15_result, error); CHECK(true_v15_result == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_schema_version(database) == 21); + CHECK(lardon3d_project_db_schema_version(database) == 22); Lardon3DProjectDbProject migrated_project; CHECK(lardon3d_project_db_get_project(database, &migrated_project) == LARDON3D_PROJECT_DB_OK && @@ -2067,7 +2085,7 @@ static bool run_test(void) { database = NULL; CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_schema_version(database) == 21); + CHECK(lardon3d_project_db_schema_version(database) == 22); lardon3d_project_db_close(database); database = NULL; CHECK(schema_compare(database_path, true_v15_path, true)); @@ -2102,7 +2120,7 @@ static bool run_test(void) { 0)); CHECK(lardon3d_project_db_open(true_v15_path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_schema_version(database) == 21); + CHECK(lardon3d_project_db_schema_version(database) == 22); lardon3d_project_db_close(database); database = NULL; @@ -2121,7 +2139,7 @@ static bool run_test(void) { 0)); CHECK(lardon3d_project_db_open(failed_v14_path, &database, error) == LARDON3D_PROJECT_DB_OK && - lardon3d_project_db_schema_version(database) == 21); + lardon3d_project_db_schema_version(database) == 22); lardon3d_project_db_close(database); database = NULL; @@ -2140,7 +2158,7 @@ static bool run_test(void) { 0)); CHECK(lardon3d_project_db_open(failed_v15_path, &database, error) == LARDON3D_PROJECT_DB_OK && - lardon3d_project_db_schema_version(database) == 21); + lardon3d_project_db_schema_version(database) == 22); lardon3d_project_db_close(database); database = NULL; @@ -2612,6 +2630,7 @@ static bool run_test(void) { CHECK(unlink(v17_path) == 0); CHECK(unlink(failed_v18_path) == 0); CHECK(unlink(failed_v19_path) == 0); + CHECK(unlink(raw_missing_table_path) == 0); CHECK(rmdir(directory) == 0); return true; } diff --git a/tests/test_raw_development.cpp b/tests/test_raw_development.cpp index 2a56d95..4b13481 100644 --- a/tests/test_raw_development.cpp +++ b/tests/test_raw_development.cpp @@ -283,6 +283,18 @@ static bool run_test() { &state, capture.capture_id, corrupt_path, 0, 1, &output); CHECK(corrupt == LARDON3D_RAW_DEVELOPMENT_UNSUPPORTED_RAW || corrupt == LARDON3D_RAW_DEVELOPMENT_CORRUPT_RAW); + CHECK(output.source_asset.asset_id != 0); + const uint64_t corrupt_asset_id = output.source_asset.asset_id; + CHECK(lardon3d_raw_develop_asset_to_capture( + &state, capture.capture_id, corrupt_asset_id, 0, 1, &output) == + LARDON3D_RAW_DEVELOPMENT_CONSTRAINT); + CHECK(lardon3d_project_db_record_capture_source_asset( + db, capture.capture_id, corrupt_asset_id, + LARDON3D_DB_CAPTURE_SOURCE_RAW) == LARDON3D_PROJECT_DB_OK); + Lardon3DRawDevelopmentResult explicit_corrupt = lardon3d_raw_develop_asset_to_capture( + &state, capture.capture_id, corrupt_asset_id, 0, 1, &output); + CHECK(explicit_corrupt == LARDON3D_RAW_DEVELOPMENT_UNSUPPORTED_RAW || + explicit_corrupt == LARDON3D_RAW_DEVELOPMENT_CORRUPT_RAW); uint64_t selected = 0; CHECK(lardon3d_project_db_get_selected_capture_image(db, capture.capture_id, &selected) == LARDON3D_PROJECT_DB_NOT_FOUND); diff --git a/tests/test_raw_development_task.cpp b/tests/test_raw_development_task.cpp new file mode 100644 index 0000000..4fe52d7 --- /dev/null +++ b/tests/test_raw_development_task.cpp @@ -0,0 +1,196 @@ +#include +#include +#include +#include + +#include +#include +#include + +extern "C" { +#include +#include +#include +#include +} + +extern "C" const Lardon3DTaskKindRegistry *lardon3d_task_kind_registry_production(void) { + static const Lardon3DTaskKindRegistry registry{}; + return ®istry; +} + +#define CHECK(condition) \ + do { \ + if (!(condition)) { \ + std::fprintf(stderr, "Failure line %d: %s\n", __LINE__, #condition); \ + return 1; \ + } \ + } while (0) + +static bool write_file(const std::string &path, const char *bytes) { + int descriptor = open(path.c_str(), O_WRONLY | O_CREAT | O_EXCL, 0600); + if (descriptor < 0) return false; + size_t size = std::strlen(bytes); + bool ok = write(descriptor, bytes, size) == static_cast(size); + return close(descriptor) == 0 && ok; +} + +int main() { + char directory_template[] = "/tmp/lardon3d-raw-task-XXXXXX"; + char *directory = mkdtemp(directory_template); + CHECK(directory != nullptr); + std::string root(directory); + std::string database_path = root + "/project.lardon3d"; + CHECK(std::filesystem::create_directories(root + "/.lardon3d/checkpoints")); + CHECK(std::filesystem::create_directories(root + "/assets/images")); + std::string raw_path = root + "/source.arw"; + std::string png_path = root + "/derived.png"; + CHECK(write_file(raw_path, "explicit raw bytes")); + CHECK(write_file(png_path, "derived png bytes")); + + char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]{}; + Lardon3DProjectDb *database = nullptr; + CHECK(lardon3d_project_db_open(database_path.c_str(), &database, error) == + LARDON3D_PROJECT_DB_OK); + Lardon3DProjectDbScanSet scanset{}; + Lardon3DProjectDbCapture capture{}; + CHECK(lardon3d_project_db_create_scanset(database, "raw-task", &scanset) == + LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_create_capture(database, scanset.scanset_id, 7, &capture) == + LARDON3D_PROJECT_DB_OK); + + Lardon3DAppState state{}; + lardon3d_app_state_init(&state); + state.project_loaded = true; + state.project_db = database; + std::snprintf(state.project_path, sizeof(state.project_path), "%s", root.c_str()); + state.hardware_profile.logical_cpu_count = 8; + state.hardware_profile.page_size_bytes = 4096; + state.hardware_profile.memory_total_bytes = UINT64_C(8) << 30; + std::snprintf(state.hardware_profile.cpu_architecture, + sizeof(state.hardware_profile.cpu_architecture), "test"); + Lardon3DResourcePolicy policy{}; + policy.maximum_cpu_load_ratio = 1.0; + policy.maximum_io_pressure_avg10 = 100.0; + policy.io_slot_capacity = 1; + state.resource_governor = lardon3d_resource_governor_create(&state.hardware_profile, &policy); + CHECK(state.resource_governor != nullptr); + + Lardon3DProjectDbImageAsset raw_asset{}; + Lardon3DProjectDbImageAsset derived_asset{}; + CHECK(lardon3d_image_catalog_publish_asset_file( + &state, raw_path.c_str(), 7, UINT64_MAX, &raw_asset) == + LARDON3D_IMAGE_CATALOG_ASSET_PUBLISHED); + CHECK(lardon3d_project_db_record_capture_source_asset( + database, capture.capture_id, raw_asset.asset_id, + LARDON3D_DB_CAPTURE_SOURCE_RAW) == LARDON3D_PROJECT_DB_OK); + + uint64_t task_id = 0; + Lardon3DTask *task = lardon3d_project_create_raw_development_task( + &state, capture.capture_id, raw_asset.asset_id, &task_id); + CHECK(task != nullptr && task_id != 0); + Lardon3DProjectDbTask generic{}; + Lardon3DProjectDbRawDevelopmentTask typed{}; + CHECK(lardon3d_project_db_load_task(database, task_id, &generic) == LARDON3D_PROJECT_DB_OK); + CHECK(generic.has_checkpoint && generic.has_task_kind && + std::strcmp(generic.task_kind, LARDON3D_RAW_DEVELOPMENT_TASK_KIND) == 0); + CHECK(lardon3d_project_db_load_raw_development_task(database, task_id, &typed) == + LARDON3D_PROJECT_DB_OK); + CHECK(typed.capture_id == capture.capture_id && typed.source_asset_id == raw_asset.asset_id && + typed.phase == LARDON3D_RAW_DEVELOPMENT_TASK_PENDING && !typed.has_image); + + Lardon3DTaskDurableSnapshot snapshot{}; + CHECK(lardon3d_task_durable_snapshot(task, &snapshot)); + Lardon3DTaskReconstructionContext reconstruction{ + root.c_str(), database, state.resource_governor, nullptr}; + Lardon3DTaskKindBinding binding{}; + CHECK(lardon3d_raw_development_task_reconstruct(&snapshot, &reconstruction, &binding)); + CHECK(binding.callback != nullptr && binding.userdata != nullptr && + binding.userdata_destroy != nullptr && binding.finished_callback != nullptr); + binding.userdata_destroy(binding.userdata); + + CHECK(lardon3d_image_catalog_publish_asset_file( + &state, png_path.c_str(), 7, UINT64_MAX, &derived_asset) == + LARDON3D_IMAGE_CATALOG_ASSET_PUBLISHED); + Lardon3DProjectDbAssetDerivation derivation{}; + derivation.parent_asset_id = raw_asset.asset_id; + derivation.child_asset_id = derived_asset.asset_id; + derivation.kind = LARDON3D_DB_ASSET_DERIVATION_GENERIC_VERSIONED; + derivation.version = 1; + derivation.has_producer_task = true; + derivation.producer_task_id = task_id; + derivation.created_at = 7; + CHECK(lardon3d_project_db_record_asset_derivation(database, &derivation) == + LARDON3D_PROJECT_DB_OK); + Lardon3DProjectDbImageRegisterStatus image_status{}; + Lardon3DProjectDbImage image{}; + CHECK(lardon3d_project_db_publish_derived_capture_image( + database, capture.capture_id, derived_asset.asset_id, "derived.png", png_path.c_str(), + task_id, 7, &image_status, &image) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_task_set_progress(task, 100, "published")); + typed.phase = LARDON3D_RAW_DEVELOPMENT_TASK_PUBLISHED; + typed.has_image = true; + typed.image_id = image.image_id; + CHECK(lardon3d_project_checkpoint_raw_development_task(&state, task, &typed) == + LARDON3D_PROJECT_TASK_CHECKPOINT_OK); + Lardon3DProjectDbRawDevelopmentTask published{}; + CHECK(lardon3d_project_db_load_raw_development_task(database, task_id, &published) == + LARDON3D_PROJECT_DB_OK); + CHECK(published.phase == LARDON3D_RAW_DEVELOPMENT_TASK_PUBLISHED && + published.image_id == image.image_id); + + Lardon3DProjectDbRawDevelopmentTask conflict = published; + conflict.source_asset_id = derived_asset.asset_id; + CHECK(lardon3d_project_checkpoint_raw_development_task(&state, task, &conflict) == + LARDON3D_PROJECT_TASK_CHECKPOINT_DB_ERROR); + CHECK(lardon3d_project_db_load_raw_development_task(database, task_id, &published) == + LARDON3D_PROJECT_DB_OK && + published.source_asset_id == raw_asset.asset_id && published.image_id == image.image_id); + + CHECK(lardon3d_project_db_record_raw_development_task( + database, &snapshot, "test.work", 1, nullptr, &typed, + 7) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + CHECK(lardon3d_project_db_record_raw_development_task( + database, &snapshot, LARDON3D_RAW_DEVELOPMENT_TASK_KIND, 2, nullptr, &typed, + 7) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + + sqlite3 *raw_database = nullptr; + char update_mismatch_kind[128]; + std::snprintf(update_mismatch_kind, sizeof(update_mismatch_kind), + "UPDATE tasks SET task_kind='test.work',task_kind_version=1 WHERE task_id=%llu", + (unsigned long long)task_id); + CHECK(sqlite3_open(database_path.c_str(), &raw_database) == SQLITE_OK); + CHECK(sqlite3_exec(raw_database, update_mismatch_kind, nullptr, nullptr, + nullptr) == SQLITE_OK); + CHECK(lardon3d_project_db_load_raw_development_task(database, task_id, &published) == + LARDON3D_PROJECT_DB_CORRUPT); + char update_recoverable_kind[128]; + std::snprintf(update_recoverable_kind, sizeof(update_recoverable_kind), + "UPDATE tasks SET task_kind='raw.develop',task_kind_version=1 " + "WHERE task_id=%llu", + (unsigned long long)task_id); + CHECK(sqlite3_exec(raw_database, update_recoverable_kind, nullptr, nullptr, + nullptr) == SQLITE_OK); + CHECK(sqlite3_close(raw_database) == SQLITE_OK); + + lardon3d_task_destroy(task); + lardon3d_resource_governor_destroy(state.resource_governor); + lardon3d_project_db_close(database); + + /* This fixture bypasses schema CHECK enforcement deliberately to prove that + * recovery treats malformed durable phase as corruption, not as pending. */ + CHECK(sqlite3_open(database_path.c_str(), &raw_database) == SQLITE_OK); + CHECK(sqlite3_exec(raw_database, "PRAGMA ignore_check_constraints=ON", nullptr, nullptr, + nullptr) == SQLITE_OK); + CHECK(sqlite3_exec(raw_database, "UPDATE raw_development_tasks SET phase=9", nullptr, nullptr, + nullptr) == SQLITE_OK); + CHECK(sqlite3_close(raw_database) == SQLITE_OK); + database = nullptr; + CHECK(lardon3d_project_db_open(database_path.c_str(), &database, error) == + LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_load_raw_development_task(database, task_id, &published) == + LARDON3D_PROJECT_DB_CORRUPT); + lardon3d_project_db_close(database); + CHECK(std::filesystem::remove_all(root) > 0); + return 0; +} diff --git a/tests/test_selected_execution.c b/tests/test_selected_execution.c new file mode 100644 index 0000000..9629759 --- /dev/null +++ b/tests/test_selected_execution.c @@ -0,0 +1,171 @@ +#include +#include +#include +#include +#include +#include + +#include + +#define CHECK(condition) do { if (!(condition)) { \ + fprintf(stderr, "selected execution failure at line %d: %s\n", __LINE__, #condition); \ + return false; } } while (0) + +static bool sql(const char *path, const char *text) { + sqlite3 *connection = NULL; + if (sqlite3_open(path, &connection) != SQLITE_OK) return false; + int code = sqlite3_exec(connection, text, NULL, NULL, NULL); + return sqlite3_close(connection) == SQLITE_OK && code == SQLITE_OK; +} + +static bool seed_v21(const char *path) { + Lardon3DProjectDb *database = NULL; + char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; + CHECK(lardon3d_project_db_open(path, &database, error) == LARDON3D_PROJECT_DB_OK); + lardon3d_project_db_close(database); + /* The two group namespaces intentionally use different values. This fixture + proves that v22 retains the explicit bridge rather than equating IDs. */ + return sql(path, + "INSERT INTO scansets(scanset_id,name,created_at,updated_at) VALUES(1,'scope',1,1);" + "INSERT INTO tasks VALUES(1,'quality','photo_quality.triage',1,5,5,100,1,0,0,0,0,1);" + "INSERT INTO tasks VALUES(2,'campaign','acquisition_campaign.run',1,5,5,100,1,0,0,0,0,1);" + "INSERT INTO photo_quality_triage_tasks VALUES(1,1,2,1,X'01');" + "INSERT INTO photo_quality_triage_results VALUES(1,1,0,1,0,1,0,100,100,100,100," + "1.0,1.0,0.0,0.0,1.0,1.0,0.0,'GOOD');" + "INSERT INTO acquisition_campaign_tasks VALUES(2,1,2,2,X'02');" + "INSERT INTO captures(capture_id,scanset_id,created_at) VALUES(1,1,1);" + "INSERT INTO acquisition_campaign_captures VALUES(2,2,1);" + "INSERT INTO image_assets(asset_id,sha256,path,size_bytes,state,created_at) " + "VALUES(1,zeroblob(32),'assets/images/00/test',1,1,1);" + "INSERT INTO image_assets(asset_id,sha256,path,size_bytes,state,created_at) " + "VALUES(2,randomblob(32),'assets/images/00/raw',1,1,1);" + "INSERT INTO capture_assets(capture_id,asset_id,role) VALUES(1,2,1);" + "INSERT INTO capture_source_assets(capture_id,asset_id,source_kind) VALUES(1,2,2);" + "INSERT INTO images(image_id,scanset_id,asset_id,original_name,source_path,imported_at) " + "VALUES(1,1,1,'selected.jpg','/source/selected.jpg',1);" + "INSERT INTO capture_images VALUES(1,1);" + "INSERT INTO sparse_calibrations VALUES(1,randomblob(32),1,1,100,100,50,50,50,50,0,0,0,0," + "2,randomblob(32));" + "INSERT INTO sparse_calibration_scopes VALUES(1,randomblob(32),1);" + "INSERT INTO sparse_calibration_scope_images VALUES(1,1,1);" + "DROP TABLE selected_execution_items;DROP TABLE selected_executions;" + "UPDATE metadata SET value=21 WHERE key='schema_version';"); +} + +static bool run(void) { + char directory[] = "/tmp/lardon3d-selected-XXXXXX"; + CHECK(mkdtemp(directory) != NULL); + char path[512]; + CHECK(snprintf(path, sizeof(path), "%s/project.db", directory) > 0); + CHECK(seed_v21(path)); + + Lardon3DProjectDb *database = NULL; + char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; + CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V22", "1", 1) == 0); + Lardon3DProjectDbResult migration_result = lardon3d_project_db_open(path, &database, error); + CHECK(migration_result != LARDON3D_PROJECT_DB_OK); + CHECK(unsetenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V22") == 0); + CHECK(sql(path, "CREATE TEMP TABLE migration_check(value INTEGER);")); + sqlite3 *raw = NULL; + CHECK(sqlite3_open(path, &raw) == SQLITE_OK); + sqlite3_stmt *query = NULL; + CHECK(sqlite3_prepare_v2(raw, "SELECT value FROM metadata WHERE key='schema_version'", -1, + &query, NULL) == SQLITE_OK); + CHECK(sqlite3_step(query) == SQLITE_ROW && sqlite3_column_int(query, 0) == 21); + sqlite3_finalize(query); + CHECK(sqlite3_close(raw) == SQLITE_OK); + + migration_result = lardon3d_project_db_open(path, &database, error); + if (migration_result != LARDON3D_PROJECT_DB_OK) + fprintf(stderr, "retry migration result=%d error=%s\n", (int)migration_result, error); + CHECK(migration_result == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_schema_version(database) == 22); + Lardon3DProjectDbSelectedExecutionItem item = { + .item_index = 0, + .quality_group_id = 1, + .campaign_group_id = 2, + .capture_id = 1, + .representation_source = LARDON3D_SELECTED_REPRESENTATION_RAW_ASSET, + .source_asset_id = 2}; + Lardon3DProjectDbSelectedExecution execution; + Lardon3DProjectDbSelectedExecutionItem unattached = item; + unattached.source_asset_id = 1; + CHECK(lardon3d_project_db_create_selected_execution(database, 1, 2, &unattached, 1, 10, + &execution) == + LARDON3D_PROJECT_DB_CONSTRAINT); + CHECK(lardon3d_project_db_create_selected_execution(database, 1, 2, &item, 1, 10, + &execution) == LARDON3D_PROJECT_DB_OK); + CHECK(execution.execution_id > 0 && execution.stage == + LARDON3D_SELECTED_EXECUTION_REPRESENTATIONS && execution.next_item_index == 0); + uint64_t execution_id = execution.execution_id; + CHECK(lardon3d_project_db_create_selected_execution(database, 1, 2, &item, 1, 10, + &execution) == LARDON3D_PROJECT_DB_OK); + CHECK(execution.execution_id == execution_id); + Lardon3DProjectDbSelectedExecutionItem conflict = item; + conflict.representation_source = LARDON3D_SELECTED_REPRESENTATION_SOURCE_IMAGE; + conflict.source_asset_id = 0; + CHECK(lardon3d_project_db_create_selected_execution(database, 1, 2, &conflict, 1, 10, + &execution) == + LARDON3D_PROJECT_DB_CONSTRAINT); + CHECK(lardon3d_project_db_record_selected_representation(database, execution_id, 0, 1, 1) == + LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_record_selected_representation(database, execution_id, 0, 1, 1) == + LARDON3D_PROJECT_DB_OK); + lardon3d_project_db_close(database); + database = NULL; + + CHECK(lardon3d_project_db_open(path, &database, error) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_load_selected_execution(database, execution_id, &execution) == + LARDON3D_PROJECT_DB_OK); + CHECK(execution.stage == LARDON3D_SELECTED_EXECUTION_CALIBRATION && + execution.next_item_index == 1 && !execution.has_calibration_scope); + Lardon3DProjectDbSelectedExecutionItem loaded; + CHECK(lardon3d_project_db_load_selected_execution_item(database, execution_id, 0, &loaded) == + LARDON3D_PROJECT_DB_OK); + CHECK(loaded.quality_group_id == 1 && loaded.campaign_group_id == 2 && + loaded.capture_id == 1 && + loaded.representation_source == LARDON3D_SELECTED_REPRESENTATION_RAW_ASSET && + loaded.source_asset_id == 2 && loaded.has_image && loaded.image_id == 1); + CHECK(lardon3d_project_db_assign_selected_calibration_scope(database, execution_id, 1) == + LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_assign_selected_calibration_scope(database, execution_id, 1) == + LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_load_selected_execution(database, execution_id, &execution) == + LARDON3D_PROJECT_DB_OK && execution.stage == LARDON3D_SELECTED_EXECUTION_READY && + execution.has_calibration_scope && execution.calibration_scope_id == 1); + CHECK(lardon3d_project_db_create_selected_execution(database, 1, 2, &item, 1, 10, + &execution) == LARDON3D_PROJECT_DB_OK && + execution.execution_id == execution_id && + execution.stage == LARDON3D_SELECTED_EXECUTION_READY); + lardon3d_project_db_close(database); + database = NULL; + + /* CHECK constraints are bypassed deliberately to verify wide-value and + stage/cursor corruption rejection at the public read boundary. */ + CHECK(sqlite3_open(path, &raw) == SQLITE_OK); + CHECK(sqlite3_exec(raw, "PRAGMA ignore_check_constraints=ON;" + "UPDATE selected_execution_items SET representation_source=1 " + "WHERE execution_id=1;", NULL, NULL, NULL) == SQLITE_OK); + CHECK(sqlite3_close(raw) == SQLITE_OK); + CHECK(lardon3d_project_db_open(path, &database, error) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_load_selected_execution_item(database, execution_id, 0, &loaded) == + LARDON3D_PROJECT_DB_CORRUPT); + lardon3d_project_db_close(database); + CHECK(sql(path, "UPDATE selected_execution_items SET representation_source=2 " + "WHERE execution_id=1;")); + + CHECK(sqlite3_open(path, &raw) == SQLITE_OK); + CHECK(sqlite3_exec(raw, "PRAGMA ignore_check_constraints=ON;" + "UPDATE selected_executions SET next_item_index=4294967296 " + "WHERE execution_id=1;", NULL, NULL, NULL) == SQLITE_OK); + CHECK(sqlite3_close(raw) == SQLITE_OK); + CHECK(lardon3d_project_db_open(path, &database, error) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_load_selected_execution(database, execution_id, &execution) == + LARDON3D_PROJECT_DB_CORRUPT); + lardon3d_project_db_close(database); + CHECK(unlink(path) == 0); + CHECK(rmdir(directory) == 0); + return true; +} + +int main(void) { return run() ? EXIT_SUCCESS : EXIT_FAILURE; }