diff --git a/docs/architecture/resource_boundary.md b/docs/architecture/resource_boundary.md index 79e2ea0..79fe245 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 **v17**; historical v16 remains frozen. +Project Database: current schema **v20**; 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/development/testing.md b/docs/development/testing.md index 0d66338..b2dba9b 100644 --- a/docs/development/testing.md +++ b/docs/development/testing.md @@ -72,6 +72,14 @@ int main(int argc, char **argv) { 6. **Déterminisme** : les tests ne dépendent pas de l'heure, du filesystem ou de l'état réseau (sauf test d'import). +## Commentaires source + +Les commentaires documentent le pourquoi et les contrats non évidents : +invariants, propriété et durée de vie, persistance, ainsi que limites et +frontières de ressources. Les API publiques documentent leurs contrats non +évidents. Ils ne paraphrasent pas le code ligne par ligne et sont mis à jour +avec tout changement de comportement. + ## Tests unitaires vs tests d'intégration `test-visual-index` couvre les descriptors synthétiques, le retrieval ORB réel, diff --git a/include/lardon3d/acquisition_campaign_task.h b/include/lardon3d/acquisition_campaign_task.h index e7ef7a4..419962a 100644 --- a/include/lardon3d/acquisition_campaign_task.h +++ b/include/lardon3d/acquisition_campaign_task.h @@ -18,8 +18,13 @@ enum { LARDON3D_ACQUISITION_CAMPAIGN_REQUEST_VERSION = 1, }; -/* Derived solely from the frozen source, path, confirmation and group bounds. - */ +/* Request wire payload is a deterministic codec, not a memcpy of in-memory structs: + - fixed magic + version prefix + - fixed-width integer fields + - explicit little-endian byte order + - bounded text fields and counts + - strict decode-side validation rejecting malformed inputs. + No native struct layout is used for persistence compatibility. */ #define LARDON3D_ACQUISITION_CAMPAIGN_TASK_REQUEST_MAX_BYTES \ ((size_t)LARDON3D_ACQUISITION_CAMPAIGN_MAX_SOURCES * \ (LARDON3D_ACQUISITION_CAMPAIGN_PATH_CAPACITY + 700u) + \ @@ -41,6 +46,9 @@ Lardon3DTask *lardon3d_project_create_acquisition_campaign_task( bool lardon3d_project_enqueue_acquisition_campaign( Lardon3DAppState *state, uint64_t scanset_id, const Lardon3DAcquisitionCampaignTaskRequest *request, uint64_t *task_id); +/* Request payload is treated as immutable durable input: task execution and + * checkpoint/recovery flow from the encoded blob, not from caller memory after + * enqueue. */ bool lardon3d_acquisition_campaign_task_reconstruct( const Lardon3DTaskDurableSnapshot *snapshot, void *context, Lardon3DTaskKindBinding *binding); diff --git a/include/lardon3d/task.h b/include/lardon3d/task.h index 2100640..b6cf0b4 100644 --- a/include/lardon3d/task.h +++ b/include/lardon3d/task.h @@ -78,7 +78,9 @@ Lardon3DTask *lardon3d_task_create_typed( Lardon3DTaskUserdataDestroy userdata_destroy ); void lardon3d_task_destroy(Lardon3DTask *task); -/* Exécute le callback dans le thread appelant. */ +/* Exécute le callback dans le thread appelant. Le callback est invoqué hors + * mutex de tâche; le contract d'exécution et l'état appartiennent à la tâche. + */ bool lardon3d_task_start( Lardon3DTask *task, Lardon3DResourceGovernor *governor, @@ -137,6 +139,9 @@ bool lardon3d_task_resource_estimate( const Lardon3DTask *task, Lardon3DResourceEstimate *estimate ); +/* L'exécution ne reçoit pas de politique d'admission : c'est au gouverneur de + * confirmer la réservation avant l'exécution. + */ bool lardon3d_task_execution_contract( const Lardon3DTask *task, Lardon3DTaskExecutionContract *contract @@ -145,7 +150,8 @@ bool lardon3d_task_execution_contract( * et met à jour le contrat. À appeler uniquement depuis le callback en cours * d'exécution. Une réponse WAIT du gouverneur est une indisponibilité * temporaire : la fonction attend un changement de ressources puis retente - * l'admission sans échouer la tâche. Retourne false si la tâche est annulée + * l'admission sans échouer la tâche. Les bornes de lot se poursuivent après + * cette nouvelle admission. Retourne false si la tâche est annulée * (TASK_CANCELLED), si le gouverneur répond REJECT ou en cas d'erreur interne * (TASK_FAILED). */ bool lardon3d_task_sequence_break( diff --git a/include/lardon3d/task_queue.h b/include/lardon3d/task_queue.h index b0c6d48..147c071 100644 --- a/include/lardon3d/task_queue.h +++ b/include/lardon3d/task_queue.h @@ -28,6 +28,10 @@ Lardon3DTaskQueue *lardon3d_task_queue_create( Lardon3DResourceGovernor *governor, size_t capacity ); +/* File d'exécution bornée, à ordre d'attente FIFO avec sélection adaptative du + * premier travail admissible, et un seul worker: ownership d'ordonnancement et + * de backpressure seulement. L'admission des demandes reste au Governneur. + */ void lardon3d_task_queue_destroy(Lardon3DTaskQueue *queue); /* La file devient propriétaire de task uniquement en cas de succès. Bloquante : attend une place libre si la file est pleine. */ diff --git a/src/acquisition_campaign_task.cpp b/src/acquisition_campaign_task.cpp index 1f993ae..709c71e 100644 --- a/src/acquisition_campaign_task.cpp +++ b/src/acquisition_campaign_task.cpp @@ -18,6 +18,9 @@ constexpr unsigned char magic[8] = {'L', '3', 'D', 'A', 'C', 'T', '1', '\0'}; constexpr size_t max_request_size = LARDON3D_ACQUISITION_CAMPAIGN_TASK_REQUEST_MAX_BYTES; +/* Codec is transport-safe and deterministic: no native struct serialization is + * used, integers are fixed-width with explicit endianness, and strings/counts are + * explicitly bounded before write/read. */ struct Writer { unsigned char *p; size_t left; @@ -206,6 +209,8 @@ bool checkpoint(Context *c, Lardon3DTask *t, uint32_t cursor) { bool run_impl(Lardon3DTask *t, void *p) { auto *c = static_cast(p); Lardon3DProjectDbAcquisitionCampaignTask persisted{}; + // Request is reconstructed from persisted blob and treated as immutable durable + // input during recovery and replay. std::vector blob(max_request_size); if (lardon3d_project_db_load_acquisition_campaign_task( c->db, lardon3d_task_id(t), blob.data(), blob.size(), &persisted) != @@ -218,6 +223,10 @@ bool run_impl(Lardon3DTask *t, void *p) { return false; Lardon3DProjectDbAcquisitionCampaignCapture retained{}; uint64_t resume = 0; + /* Durable replay uses the stable mapping (task_id, group_id) -> capture_id. + * The persisted cursor is the zero-based next-work position: after group N + * retention, its numeric value is N and replay materializes group N + 1. + */ auto lr = lardon3d_project_db_load_acquisition_campaign_capture( c->db, lardon3d_task_id(t), group_id, &retained); if (lr == LARDON3D_PROJECT_DB_OK) @@ -248,6 +257,10 @@ bool run_impl(Lardon3DTask *t, void *p) { c->db, lardon3d_task_id(t), group_id, out.groups[0].capture_id, group_id) != LARDON3D_PROJECT_DB_OK) return lardon3d_task_fail(t, "Rétention de Capture impossible."); + /* Limite de reprise acceptée: entre le retour de S3-E et cette rétention + * durable, une identité de capture ne peut pas être déduite à posteriori + * depuis les chemins/métadonnées/ID d'image. + */ #ifdef LARDON3D_ACQUISITION_CAMPAIGN_TASK_TESTING const char *after_retention = std::getenv("LARDON3D_TEST_CAMPAIGN_FAIL_AFTER_RETENTION"); @@ -261,6 +274,8 @@ bool run_impl(Lardon3DTask *t, void *p) { !checkpoint(c, t, group_id)) return lardon3d_task_fail(t, "Checkpoint de campagne impossible."); if (group_id < c->plan.group_count) { + // sequence_break: libère la réservation courante et renégocie l'admission + // via le Governor avant le groupe suivant. Lardon3DTaskExecutionContract contract{}; Lardon3DResourceReservation *reservation = nullptr; if (!lardon3d_task_sequence_break(t, c->governor, &reservation, @@ -352,6 +367,7 @@ bool request_decode_impl( unsigned char m[8]; uint32_t version, n, c, rep, select; uint64_t imported, maxbytes; + /* Version 1 is the only accepted codec shape; any mismatch rejects replay. */ if (!q.bytes(m, 8) || std::memcmp(m, magic, 8) || !q.u32(version) || version != 1 || !q.u32(n) || n == 0 || n > sc || n > LARDON3D_ACQUISITION_CAMPAIGN_MAX_SOURCES || !q.u32(c) || c > cc || @@ -490,6 +506,7 @@ Lardon3DTask *create_task_impl( return t; } +/* C ABI boundary: exceptions are trapped so callers never receive C++ throws. */ extern "C" bool lardon3d_acquisition_campaign_request_encode( const Lardon3DAcquisitionCampaignTaskRequest *r, unsigned char *out, size_t cap, size_t *size) { diff --git a/src/acquisition_pairing.cpp b/src/acquisition_pairing.cpp index 0bdae7f..90f7316 100644 --- a/src/acquisition_pairing.cpp +++ b/src/acquisition_pairing.cpp @@ -141,6 +141,8 @@ enum class JpegValidationResult { constexpr size_t JPEG_MAX_CONTAINER_IMAGES = 8u; +// Structural validation deliberately parses JPEG framing without decoding +// pixels, so metadata acceptance does not depend on decoder permissiveness. bool read_byte(FILE *file, unsigned char &value) { const int byte = std::fgetc(file); if (byte == EOF) { @@ -234,6 +236,8 @@ JpegValidationResult validate_jpeg_image(FILE *file, bool soi_already_consumed, } } + // Entropy bytes are not marker payload: FF 00 is stuffing and RSTn stays + // within the scan. Only a real following marker resumes marker parsing. bool in_entropy = false; for (;;) { unsigned char marker = 0u; @@ -283,6 +287,9 @@ JpegValidationResult validate_jpeg_image(FILE *file, bool soi_already_consumed, } JpegValidationResult validate_jpeg_structure(FILE *file) { + // Secondary JPEGs are allowed only in the bounded private container form: + // primary APP2 contains MPF\0, every image reaches EOI structurally, and + // inter-image/final padding is zero only. No image pixels are decoded. bool primary_has_mpf = false; for (size_t image_index = 0u; image_index < JPEG_MAX_CONTAINER_IMAGES; ++image_index) { bool image_has_mpf = false; diff --git a/src/feature_store.c b/src/feature_store.c index 05d8303..d99931e 100644 --- a/src/feature_store.c +++ b/src/feature_store.c @@ -17,6 +17,11 @@ static const unsigned char feature_magic[8] = {'L', '3', 'D', 'F', 'E', 'A', 'T' _Static_assert(sizeof(float) == 4 && FLT_RADIX == 2 && FLT_MANT_DIG == 24 && FLT_MAX_EXP == 128, "Feature File v1/v2 exige IEEE-754 binary32."); +/* L3DFEAT v1/v2 uses explicit fixed-width little-endian headers, never native + * structs. Counts, offsets, dimensions, source SHA-256, and parameter + * fingerprint bind descriptors to their input; malformed/truncated or unknown + * versions are rejected before feature data is consumed. */ + struct Lardon3DFeatureReader { int descriptor; uint64_t keypoint_offset; diff --git a/src/project_db.c b/src/project_db.c index 11e1aa6..572f0da 100644 --- a/src/project_db.c +++ b/src/project_db.c @@ -608,6 +608,8 @@ static const char schema_capture_provenance_v19[] = "CHECK(parent_asset_id!=child_asset_id));"; static const char schema_acquisition_campaign_v20[] = + /* v20 is additive: the generic Task row owns runtime state, while the + typed row owns the immutable campaign request and durable cursor. */ "CREATE TABLE IF NOT EXISTS acquisition_campaign_tasks(" "task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) ON DELETE CASCADE," "scanset_id INTEGER NOT NULL REFERENCES scansets(scanset_id)," @@ -1107,6 +1109,8 @@ static Lardon3DProjectDbResult migrate(Lardon3DProjectDb *database, unsigned int } } if (result == LARDON3D_PROJECT_DB_OK && from_version < 20) { + /* The additive v19->v20 DDL remains inside the surrounding migration + transaction; schema metadata advances only after this succeeds. */ result = execute(database, schema_acquisition_campaign_v20, "migrate acquisition campaign task v19 to v20"); #ifdef LARDON3D_PROJECT_DB_TESTING @@ -2247,6 +2251,8 @@ Lardon3DProjectDbResult lardon3d_project_db_record_acquisition_campaign_task( !parameters->request || parameters->request_size == 0 || parameters->request_size > INT_MAX) { return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; } + /* The generic Task/checkpoint and typed request persist atomically: recovery + 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, updated_at); @@ -2292,6 +2298,11 @@ Lardon3DProjectDbResult lardon3d_project_db_load_acquisition_campaign_task( Lardon3DProjectDbResult lardon3d_project_db_retain_acquisition_campaign_capture( Lardon3DProjectDb *database, uint64_t task_id, uint32_t group_id, uint64_t capture_id, uint32_t next_group_id) { + /* S3-E already returned capture_id. This transaction retains the group + mapping and advances the one-based cursor together; generic progress and + checkpoint advance afterwards. An interruption after S3-E returns and + before this retention transaction cannot safely be repaired by guessing + identity from paths, hashes, metadata, or image IDs. */ if (!database || !valid_task_id(task_id) || !valid_task_id(capture_id) || group_id == 0 || next_group_id != group_id) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; (void)pthread_mutex_lock(&database->mutex); diff --git a/src/raw_development.cpp b/src/raw_development.cpp index 09893d8..b2c1903 100644 --- a/src/raw_development.cpp +++ b/src/raw_development.cpp @@ -33,6 +33,8 @@ constexpr uint32_t kBackendKindLibRaw = 1; constexpr uint32_t kEncoderKindOpenCvPng = 1; constexpr size_t kRawFingerprintPayloadSize = 200; +// RAW Policy v1 is part of derived scientific identity. Its fixed +// little-endian L3DRAWD1 payload is not a pathname-derived source identity. bool valid_wb(const float wb[4]) { for (size_t i = 0; i < 4; ++i) if (!std::isfinite(wb[i]) || wb[i] <= 0.0F) return false; return true; @@ -175,6 +177,8 @@ extern "C" Lardon3DRawDevelopmentResult lardon3d_raw_development_policy_fingerpr return LARDON3D_RAW_DEVELOPMENT_INTERNAL_ERROR; std::vector bytes; bytes.reserve(kRawFingerprintPayloadSize); + // Record all policy choices and relevant backend versions; decoder + // defaults must never create an untracked representation. const char magic[] = "L3DRAWD1"; bytes.insert(bytes.end(), magic, magic + sizeof(magic) - 1); put_u32(&bytes, 1); put_u32(&bytes, kBackendKindLibRaw); @@ -247,6 +251,9 @@ extern "C" Lardon3DRawDevelopmentResult lardon3d_raw_develop_to_capture( if (!verify_managed_asset(managed_source, output->source_asset.sha256)) return LARDON3D_RAW_DEVELOPMENT_SOURCE_CHANGED; LibRaw raw; raw.imgdata.params.user_qual = 3; raw.imgdata.params.half_size = 0; + // The resolved WB is explicit. Automatic WB, brightness, and color + // choices are disabled so policy rather than hidden decoder state defines + // the derived asset. raw.imgdata.params.user_flip = 0; raw.imgdata.params.use_auto_wb = 0; raw.imgdata.params.use_camera_wb = 0; raw.imgdata.params.use_camera_matrix = 0; raw.imgdata.params.output_profile = nullptr; raw.imgdata.params.camera_profile = nullptr; diff --git a/src/task.c b/src/task.c index 8c6adc5..a661d49 100644 --- a/src/task.c +++ b/src/task.c @@ -220,6 +220,9 @@ lardon3d_task_start( const Lardon3DResourceReservation *reservation ) { + /* Start assumes admission/contract was already decided by the queue/ governor. + * Task only installs the provided active reservation and owns execution state. + */ Lardon3DResourceReservationInfo information; if (!task || !lardon3d_resource_reservation_get_active( governor, @@ -285,6 +288,10 @@ lardon3d_task_start( copy_text(task->message, sizeof(task->message), "Tâche en cours."); (void)pthread_mutex_unlock(&task->mutex); + /* Callback is executed without task mutex held; caller-visible execution + * state transitions remain owned by task internals only. + * Finished callback is also issued after lock release. + */ bool succeeded = task->callback(task, task->userdata); (void)pthread_mutex_lock(&task->mutex); @@ -563,6 +570,9 @@ lardon3d_task_sequence_break( .gpu_slots = information.gpu_slots, .io_slots = information.io_slots, }; + // sequence_break releases one active reservation and acquires the + // next one before returning; tasks should continue only from a + // cleanly admitted boundary. task->has_contract = true; ++task->sequence_count; *out_reservation = next; diff --git a/src/task_checkpoint.c b/src/task_checkpoint.c index f35ba64..b2e9f59 100644 --- a/src/task_checkpoint.c +++ b/src/task_checkpoint.c @@ -20,6 +20,10 @@ static const unsigned char checkpoint_magic[8] = { 'L', '3', 'D', 'T', 'A', 'S', 'K', '\0' }; +/* Checkpoint v1 is fixed-size and field-by-field little-endian: magic, + * version, size, checksum, then generic runtime state. Task-kind payloads, + * callbacks, locks, and reservations are intentionally transient and absent. */ + static void put_u32(unsigned char *output, uint32_t value) { @@ -288,6 +292,10 @@ lardon3d_task_checkpoint_save( const Lardon3DTaskDurableSnapshot *snapshot ) { + /* The temporary file is synced before rename, then its parent directory is + * synced. Before rename the prior checkpoint remains authoritative; after + * rename a directory-sync failure is reported as published-not-durable, + * never as a rollbackable write. */ if (!path || !path[0] || !valid_snapshot(snapshot)) { return LARDON3D_TASK_CHECKPOINT_INVALID; } diff --git a/src/task_queue.c b/src/task_queue.c index 7e16980..3934e67 100644 --- a/src/task_queue.c +++ b/src/task_queue.c @@ -16,6 +16,10 @@ enum { LARDON3D_PENDING_RESOURCE_WAIT_MILLISECONDS = 500, }; +/* The queue owns submission order and bounded backpressure only. + * It does not own resource policy; every admissible decision comes from the + * Governor via reserve/evaluate calls. + */ struct Lardon3DTaskQueue { pthread_mutex_t mutex; pthread_cond_t not_empty; @@ -61,7 +65,9 @@ unlink_pending(Lardon3DTaskQueue *queue, TaskNode *previous, TaskNode *node) (void)pthread_cond_signal(&queue->not_full); } -/* Parcourt la file d'attente et sélectionne la première tâche admissible. +/* Parcourt la file d'attente dans son ordre FIFO et sélectionne la première + * tâche admissible; une attente de ressources peut donc laisser passer une + * tâche antérieure sans lui faire perdre sa place dans la file. * Les tâches terminales ou refusées sont retirées de la file d'attente. * Une tâche en attente de ressources reste en file et sera réévaluée. * Retourne NULL si aucune tâche ne peut démarrer immédiatement. */ @@ -154,6 +160,10 @@ static void * queue_worker(void *context) { Lardon3DTaskQueue *queue = context; + /* Single worker only; execution is serialized by design. Queue preserves + * pending FIFO order with adaptive dispatch/backpressure; Governor decides + * admission. + */ for (;;) { (void)pthread_mutex_lock(&queue->mutex); while (!queue->stopping && !queue->pending_head) {