Arachne 1.0
Arachne - the perpetual stitcher of Wikidata entities.
Loading...
Searching...
No Matches
arachne::penelope::store Class Referencefinal

#include <include/penelope/store.hpp>

Public Member Functions

 store (std::filesystem::path root)
snapshot_result replace_candidate_snapshot (const candidate_snapshot_request &request)
std::optional< snapshot_resultactive_snapshot (graph_domain domain) const
integrity_report integrity_check (graph_domain domain, const std::filesystem::path &database_path) const
void checkpoint_staging (graph_domain domain, const std::filesystem::path &database_path) const
std::string export_jsonl (graph_domain domain, const std::filesystem::path &database_path, const std::filesystem::path &destination) const
const std::filesystem::path & root () const noexcept

Private Attributes

std::filesystem::path root_

Detailed Description

Penelope's isolated SQLite persistence boundary.

Candidate builds always start from an empty database and therefore cannot carry grey/group/greedy state from an older build. Product data is maintained directly by the dedicated inbox importer; it is deliberately outside this immutable candidate-snapshot store.

Canonical SQLite files contain research data only. Run IDs, payload paths, and artifact hashes are written exclusively to metadata.json beside the database.

Definition at line 71 of file store.hpp.

Constructor & Destructor Documentation

◆ store()

arachne::penelope::store::store ( std::filesystem::path root)
explicit

Definition at line 1283 of file store.cpp.

1283 {
1284 if (root.empty()) {
1285 fail("Penelope store root must not be empty");
1286 }
1287 root_ = fs::absolute(std::move(root)).lexically_normal();
1288 if (root_ == root_.root_path()) {
1289 fail("Penelope store root must not be a filesystem root");
1290 }
1291 for (const auto& component : root_) {
1292 if (component == "inbox") {
1293 fail(
1294 "Penelope store root must never be inside the immutable inbox"
1295 );
1296 }
1297 }
1298 fs::create_directories(
1300 );
1301 fs::create_directories(
1303 );
1304}
const std::filesystem::path & root() const noexcept
Definition store.hpp:101
std::filesystem::path root_
Definition store.hpp:106
fs::path domain_path(const fs::path &root, graph_domain domain)
Definition store.cpp:411

Member Function Documentation

◆ active_snapshot()

std::optional< snapshot_result > arachne::penelope::store::active_snapshot ( graph_domain domain) const
nodiscard

Return the currently active immutable snapshot, if one exists.

Definition at line 1307 of file store.cpp.

1307 {
1308 const std::string id = read_active_id(root_, domain);
1309 if (id.empty()) {
1310 return std::nullopt;
1311 }
1312 return snapshot_from_directory(root_, domain, id, true);
1313}
snapshot_result snapshot_from_directory(const fs::path &root, graph_domain domain, std::string id, bool verify_hashes)
Definition store.cpp:1192
std::string read_active_id(const fs::path &root, graph_domain domain)
Definition store.cpp:415

◆ checkpoint_staging()

void arachne::penelope::store::checkpoint_staging ( graph_domain domain,
const std::filesystem::path & database_path ) const

Checkpoint a staging database and require a clean integrity report. This must not be used to modify a database referenced by ACTIVE.

Definition at line 1323 of file store.cpp.

1325 {
1326 if (const auto active = active_snapshot(domain)) {
1327 std::error_code error;
1328 if (fs::equivalent(active->database_path, database_path, error)
1329 && !error) {
1330 fail("refusing to checkpoint an active immutable snapshot");
1331 }
1332 }
1333 seal_and_validate_database(domain, database_path);
1334 remove_staging_sqlite_sidecars(database_path, database_path.parent_path());
1335}
std::optional< snapshot_result > active_snapshot(graph_domain domain) const
Definition store.cpp:1307
void remove_staging_sqlite_sidecars(const fs::path &database_path, const fs::path &expected_staging_directory)
Definition store.cpp:473
void seal_and_validate_database(const graph_domain domain, const fs::path &database_path)
Definition store.cpp:1105

References arachne::penelope::anonymous_namespace{store.cpp}::remove_staging_sqlite_sidecars(), and arachne::penelope::anonymous_namespace{store.cpp}::seal_and_validate_database().

Referenced by replace_candidate_snapshot().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ export_jsonl()

std::string arachne::penelope::store::export_jsonl ( graph_domain domain,
const std::filesystem::path & database_path,
const std::filesystem::path & destination ) const
nodiscard

Write the stable, table-ordered JSONL representation of a snapshot.

Definition at line 1337 of file store.cpp.

1340 {
1341 database db(
1342 database_path, SQLITE_OPEN_READONLY, database_access::immutable_readonly
1343 );
1344 db.exec("PRAGMA foreign_keys = ON");
1345 if (!destination.parent_path().empty()) {
1346 fs::create_directories(destination.parent_path());
1347 }
1348 std::ofstream output(destination, std::ios::binary | std::ios::trunc);
1349 if (!output) {
1350 fail("cannot create deterministic export: " + destination.string());
1351 }
1352 for (const auto& table : export_tables(domain)) {
1353 statement rows(
1354 db.get(),
1355 "SELECT * FROM " + std::string(table.name) + " ORDER BY "
1356 + std::string(table.order)
1357 );
1358 while (rows.row()) {
1359 json values = json::object();
1360 const int columns = sqlite3_column_count(rows.get());
1361 for (int column = 0; column < columns; ++column) {
1362 values[sqlite3_column_name(rows.get(), column)]
1363 = sqlite_value(rows.get(), column);
1364 }
1365 json line { { "table", table.name }, { "row", std::move(values) } };
1366 output << canonical_json(line) << '\n';
1367 if (!output) {
1368 fail("failed while writing deterministic export");
1369 }
1370 }
1371 }
1372 output.flush();
1373 if (!output) {
1374 fail("failed while flushing deterministic export");
1375 }
1376 output.close();
1377 return crypto::sha256_file(destination);
1378}
std::string sha256_file(const std::filesystem::path &path)
Definition crypto.cpp:221
const std::vector< export_table > & export_tables(graph_domain domain)
Definition store.cpp:1153
json sqlite_value(sqlite3_stmt *statement_value, int column)
Definition store.cpp:1164
std::string canonical_json(const nlohmann::json &document)

References arachne::penelope::anonymous_namespace{store.cpp}::database::database(), arachne::penelope::anonymous_namespace{store.cpp}::immutable_readonly, and arachne::crypto::sha256_file().

Referenced by replace_candidate_snapshot().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ integrity_check()

integrity_report arachne::penelope::store::integrity_check ( graph_domain domain,
const std::filesystem::path & database_path ) const
nodiscard

Run SQLite integrity, FK, and domain-specific structural checks.

Definition at line 1315 of file store.cpp.

1317 {
1318 return inspect_database(
1319 domain, database_path, database_access::immutable_readonly
1320 );
1321}
integrity_report inspect_database(graph_domain domain, const fs::path &database_path, const database_access access=database_access::ordinary)
Definition store.cpp:1038

References arachne::penelope::anonymous_namespace{store.cpp}::immutable_readonly, and arachne::penelope::anonymous_namespace{store.cpp}::inspect_database().

Here is the call graph for this function:

◆ replace_candidate_snapshot()

snapshot_result arachne::penelope::store::replace_candidate_snapshot ( const candidate_snapshot_request & request)
nodiscard

Definition at line 1523 of file store.cpp.

1523 {
1524 if (request.run_id.empty()) {
1525 fail("candidate snapshot request requires a run_id");
1526 }
1527 const auto& descriptor = request.plan;
1528 if (!fs::is_regular_file(descriptor.control_contract_path)
1529 || !fs::is_regular_file(descriptor.resolved_plan_payload_path)) {
1530 fail("candidate control contract and resolved payload must both exist");
1531 }
1532 const json control = read_json(descriptor.control_contract_path);
1535 control, "candidate plan"
1536 );
1537 const auto& plan_artifact = control.at("plan_artifact");
1538 const std::string payload_hash = lowercase(
1539 require_string(plan_artifact, "sha256", "control.plan_artifact")
1540 );
1541 require_sha256(payload_hash, "control.plan_artifact.sha256");
1542 if (crypto::sha256_file(descriptor.resolved_plan_payload_path)
1543 != payload_hash) {
1544 fail(
1545 "resolved candidate plan payload hash does not match control "
1546 "contract"
1547 );
1548 }
1549 const auto expected_bytes
1550 = plan_artifact.at("byte_length").get<std::uint64_t>();
1551 if (fs::file_size(descriptor.resolved_plan_payload_path)
1552 != expected_bytes) {
1553 fail(
1554 "resolved candidate plan payload length does not match control "
1555 "contract"
1556 );
1557 }
1558 const json payload
1559 = read_candidate_payload(descriptor.resolved_plan_payload_path);
1560 const std::string seed = request.run_id + "\n"
1561 + require_string(control, "plan_id", "candidate control") + "\n"
1562 + payload_hash;
1563 staging_guard staging {
1565 };
1566 const fs::path database_path = staging.path / "graph.sqlite";
1567 {
1568 database db(database_path, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE);
1571 transaction build(db);
1572 import_candidate_plan(db.get(), control, payload);
1573 build.commit();
1574 }
1576 const fs::path export_path = staging.path / "graph.jsonl";
1577 const std::string export_hash
1578 = export_jsonl(graph_domain::candidate, database_path, export_path);
1579 const std::string database_hash = crypto::sha256_file(database_path);
1580 remove_staging_sqlite_sidecars(database_path, staging.path);
1581 const std::string snapshot_id = "candidate_" + export_hash.substr(0, 32);
1582 const json validation_artifact = write_validation_report(
1583 staging.path, graph_domain::candidate, snapshot_id
1584 );
1585 const std::string database_ref
1586 = "candidate/snapshots/" + snapshot_id + "/graph.sqlite";
1587 const std::string export_ref
1588 = "candidate/snapshots/" + snapshot_id + "/graph.jsonl";
1589 const json metadata {
1590 { "contract", candidate_contract },
1591 { "format_version", 1 },
1592 { "snapshot_id", snapshot_id },
1593 { "run_id", request.run_id },
1594 { "graph_version", "candidate-schema-v1" },
1595 { "content_sha256", database_hash },
1596 { "database",
1597 artifact(
1598 database_ref, database_hash, fs::file_size(database_path),
1599 "application/vnd.sqlite3"
1600 ) },
1601 { "exports",
1602 json::array(
1603 { { { "kind", "candidate-jsonl" },
1604 { "artifact",
1605 artifact(
1606 export_ref, export_hash, fs::file_size(export_path),
1607 "application/x-ndjson"
1608 ) } } }
1609 ) },
1610 { "plan_id", control.at("plan_id") },
1611 { "source_snapshot_id",
1612 control.at("source_snapshot").at("snapshot_id") },
1613 { "activated_at", utc_now() },
1614 { "structural_validation",
1615 { { "passed", true }, { "report", validation_artifact } } },
1616 { "extensions",
1617 { { "org.ninjaro.penelope",
1618 { { "control_contract_ref",
1619 descriptor.control_contract_path.generic_string() },
1620 { "control_contract_sha256",
1621 crypto::sha256_file(descriptor.control_contract_path) },
1622 { "plan_artifact", plan_artifact } } } } },
1623 };
1626 research_candidate_graph_snapshot,
1627 metadata, "candidate snapshot metadata"
1628 );
1630 staging.path / "metadata.json", canonical_json(metadata) + "\n"
1631 );
1632 return finalize_snapshot(
1633 root_, graph_domain::candidate, staging, snapshot_id, database_hash,
1634 export_hash, 1, 0
1635 );
1636}
void checkpoint_staging(graph_domain domain, const std::filesystem::path &database_path) const
Definition store.cpp:1323
std::string export_jsonl(graph_domain domain, const std::filesystem::path &database_path, const std::filesystem::path &destination) const
Definition store.cpp:1337
json read_json(const fs::path &path, const std::uintmax_t maximum_bytes, const std::string_view description)
Definition main.cpp:142
void require_valid_contract(arachnespace::contracts::contract_name expected, const json &document, std::string_view context)
Definition store.cpp:1382
json artifact(std::string storage_ref, std::string sha256, std::uintmax_t byte_length, std::string media_type)
Definition store.cpp:1484
std::string lowercase(std::string value)
Definition store.cpp:279
void create_candidate_schema(const database &db)
Definition store.cpp:405
constexpr std::string_view candidate_contract
Definition store.cpp:40
void import_candidate_plan(sqlite3 *db, const json &control, const json &payload)
Definition store.cpp:594
json write_validation_report(const fs::path &staging_path, graph_domain domain, std::string_view snapshot_id)
Definition store.cpp:1496
fs::path make_staging_directory(const fs::path &root, graph_domain domain, std::string_view seed)
Definition store.cpp:451
json read_candidate_payload(const fs::path &path)
Definition store.cpp:1400
void configure_connection(const database &db)
Definition store.cpp:381
snapshot_result finalize_snapshot(const fs::path &root, graph_domain domain, staging_guard &staging, std::string snapshot_id, std::string database_hash, std::string export_hash, std::size_t applied, std::size_t skipped)
Definition store.cpp:1240
void write_bytes(const fs::path &path, std::string_view bytes)
Definition store.cpp:267
const json * require_string(const json &object, const std::string_view key, const std::string_view path, validation_result &result)
void require_sha256(const json &object, const std::string_view key, const std::string_view path, validation_result &result)

References arachne::penelope::candidate, checkpoint_staging(), arachne::penelope::anonymous_namespace{store.cpp}::transaction::commit(), arachne::penelope::anonymous_namespace{store.cpp}::configure_connection(), arachne::penelope::anonymous_namespace{store.cpp}::create_candidate_schema(), arachne::penelope::anonymous_namespace{store.cpp}::database::database(), export_jsonl(), arachne::penelope::anonymous_namespace{store.cpp}::database::get(), arachne::penelope::anonymous_namespace{store.cpp}::require_sha256(), arachnespace::contracts::research_candidate_graph_plan, arachnespace::contracts::research_candidate_graph_snapshot, arachne::penelope::candidate_snapshot_request::run_id, arachne::crypto::sha256_file(), and arachne::penelope::anonymous_namespace{store.cpp}::transaction::transaction().

Referenced by anonymous_namespace{main.cpp}::command_candidate_rebuild().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ root()

const std::filesystem::path & arachne::penelope::store::root ( ) const
inlinenodiscardnoexcept

Definition at line 101 of file store.hpp.

101 {
102 return root_;
103 }

Member Data Documentation

◆ root_

std::filesystem::path arachne::penelope::store::root_
private

Definition at line 106 of file store.hpp.


The documentation for this class was generated from the following files: