diff --git a/docs/projector-pattern.md b/docs/projector-pattern.md index b535145..75c07c1 100644 --- a/docs/projector-pattern.md +++ b/docs/projector-pattern.md @@ -121,6 +121,21 @@ for (;;) { * Cursor paging boundaries MUST NOT affect results. * A checkpoint MUST allow resume without changing derived outcomes. +### 4.3 Implementation Slice (Checklist + Stub) + +**Checklist (minimal, repo-aligned):** + +* Choose a stable `projection_name`, `projection_schema_version`, and `builder_id`. +* Read authoritative head state via `amduat_asl_index_current_state`. +* Page log records with `amduat_asl_log_read_since` (cursor-driven). +* Apply a deterministic projection step (even if it is just a counter in the stub). +* Encode a checkpoint via `amduat_asl_projection_checkpoint_encode_v1`. +* Store the checkpoint as an ASL record with schema `projection/checkpoint`. +* Verify the stored checkpoint decodes and matches the last processed `log_position`. + +**Repo stub:** `tests/asl/test_asl_projection_replay.c` includes +`test_projector_loop_stub()` that runs the minimal loop and persists a checkpoint. + --- ## 5. Projection Checkpoint Records diff --git a/tests/asl/test_asl_projection_replay.c b/tests/asl/test_asl_projection_replay.c index 54c1ef2..96b1dec 100644 --- a/tests/asl/test_asl_projection_replay.c +++ b/tests/asl/test_asl_projection_replay.c @@ -351,6 +351,168 @@ cleanup_root: return exit_code; } +static int test_projector_loop_stub(void) { + amduat_asl_store_config_t config; + amduat_asl_store_index_fs_t fs; + amduat_asl_store_t store; + amduat_asl_index_state_t state; + amduat_asl_log_record_t *page = NULL; + size_t page_len = 0u; + amduat_asl_log_position_t after = 0u; + amduat_asl_log_position_t next = 0u; + bool end = false; + amduat_reference_t checkpoint_ref; + amduat_asl_record_t checkpoint_record; + amduat_asl_projection_checkpoint_v1_t checkpoint; + amduat_asl_projection_checkpoint_v1_t decoded; + amduat_octets_t checkpoint_bytes = amduat_octets(NULL, 0u); + size_t projected_count = 0u; + char *root; + int exit_code = 1; + + root = make_temp_root(); + if (root == NULL) { + fprintf(stderr, "temp root failed\n"); + return 1; + } + + memset(&config, 0, sizeof(config)); + config.encoding_profile_id = AMDUAT_ENC_ASL1_CORE_V1; + config.hash_id = AMDUAT_HASH_ASL1_ID_SHA256; + if (!amduat_asl_store_index_fs_init(&fs, config, root)) { + fprintf(stderr, "index fs init failed\n"); + goto cleanup_root; + } + amduat_asl_store_init(&store, config, amduat_asl_store_index_fs_ops(), &fs); + + { + uint8_t payload_a[2] = {0x10, 0x20}; + uint8_t payload_b[2] = {0x30, 0x40}; + amduat_reference_t ref = amduat_reference(0u, amduat_octets(NULL, 0u)); + amduat_artifact_t artifact = amduat_artifact(amduat_octets(payload_a, sizeof(payload_a))); + if (amduat_asl_store_put_indexed(&store, artifact, &ref, &state) != + AMDUAT_ASL_STORE_OK) { + fprintf(stderr, "put_indexed A failed\n"); + amduat_reference_free(&ref); + goto cleanup_root; + } + amduat_reference_free(&ref); + + artifact = amduat_artifact(amduat_octets(payload_b, sizeof(payload_b))); + ref = amduat_reference(0u, amduat_octets(NULL, 0u)); + if (amduat_asl_store_put_indexed(&store, artifact, &ref, &state) != + AMDUAT_ASL_STORE_OK) { + fprintf(stderr, "put_indexed B failed\n"); + amduat_reference_free(&ref); + goto cleanup_root; + } + amduat_reference_free(&ref); + } + + if (!amduat_asl_index_current_state(&store, &state)) { + fprintf(stderr, "current_state failed\n"); + goto cleanup_root; + } + + checkpoint_ref = amduat_reference(0u, amduat_octets(NULL, 0u)); + while (!end) { + if (!amduat_asl_log_read_since(&store, + after, + 0u, + &page, + &page_len, + &next, + &end)) { + fprintf(stderr, "projector log read failed\n"); + goto cleanup_checkpoint_ref; + } + + projected_count += page_len; + + memset(&checkpoint, 0, sizeof(checkpoint)); + checkpoint.checkpoint_version = AMDUAT_ASL_PROJECTION_CHECKPOINT_VERSION_V1; + checkpoint.projection_name = + amduat_octets("projector-stub", strlen("projector-stub")); + checkpoint.projection_schema_version = 1u; + checkpoint.has_snapshot_id = true; + checkpoint.snapshot_id = state.snapshot_id; + checkpoint.log_position = next; + checkpoint.created_at = 0u; + checkpoint.builder_id = + amduat_octets("test-projector", strlen("test-projector")); + checkpoint.has_artifact_count = true; + checkpoint.artifact_count = projected_count; + + if (!amduat_asl_projection_checkpoint_encode_v1(&checkpoint, + &checkpoint_bytes)) { + fprintf(stderr, "checkpoint encode failed\n"); + amduat_enc_asl_log_free(page, page_len); + goto cleanup_checkpoint_ref; + } + + amduat_reference_free(&checkpoint_ref); + if (amduat_asl_record_store_put( + &store, + amduat_octets("projection/checkpoint", + strlen("projection/checkpoint")), + checkpoint_bytes, + &checkpoint_ref) != AMDUAT_ASL_STORE_OK) { + fprintf(stderr, "checkpoint store failed\n"); + amduat_octets_free(&checkpoint_bytes); + amduat_enc_asl_log_free(page, page_len); + goto cleanup_checkpoint_ref; + } + + amduat_octets_free(&checkpoint_bytes); + amduat_enc_asl_log_free(page, page_len); + after = next; + } + + if (amduat_asl_record_store_get(&store, checkpoint_ref, &checkpoint_record) != + AMDUAT_ASL_STORE_OK) { + fprintf(stderr, "checkpoint record get failed\n"); + goto cleanup_checkpoint_ref; + } + + if (!amduat_octets_eq( + checkpoint_record.schema, + amduat_octets("projection/checkpoint", + strlen("projection/checkpoint")))) { + fprintf(stderr, "checkpoint schema mismatch\n"); + amduat_asl_record_free(&checkpoint_record); + goto cleanup_checkpoint_ref; + } + + if (!amduat_asl_projection_checkpoint_decode_v1( + checkpoint_record.payload, &decoded)) { + fprintf(stderr, "checkpoint decode failed\n"); + amduat_asl_record_free(&checkpoint_record); + goto cleanup_checkpoint_ref; + } + + if (decoded.log_position != state.log_position) { + fprintf(stderr, "checkpoint log position mismatch\n"); + amduat_asl_projection_checkpoint_free(&decoded); + amduat_asl_record_free(&checkpoint_record); + goto cleanup_checkpoint_ref; + } + + amduat_asl_projection_checkpoint_free(&decoded); + amduat_asl_record_free(&checkpoint_record); + exit_code = 0; + +cleanup_checkpoint_ref: + amduat_octets_free(&checkpoint_bytes); + amduat_reference_free(&checkpoint_ref); +cleanup_root: + if (!remove_tree(root)) { + fprintf(stderr, "cleanup failed\n"); + exit_code = 1; + } + free(root); + return exit_code; +} + int main(void) { if (test_cursor_deterministic_replay() != 0) { return 1; @@ -358,5 +520,8 @@ int main(void) { if (test_checkpoint_resume() != 0) { return 1; } + if (test_projector_loop_stub() != 0) { + return 1; + } return 0; }