Arachne 1.0
Arachne - the perpetual stitcher of Wikidata entities.
Loading...
Searching...
No Matches
anonymous_namespace{main.cpp} Namespace Reference

Classes

class  cli_error
class  options
struct  configuration
struct  written_run_manifest
struct  resolved_snapshot_export

Typedefs

using json = nlohmann::json
using ordered_json = nlohmann::ordered_json

Enumerations

enum class  cocoon_status
enum class  cocoon_status

Functions

fs::path repository_root ()
fs::path resolved_path (const fs::path &path, const fs::path &relative_root)
std::optional< fs::path > conventional_legacy_inbox ()
std::string read_bytes (const fs::path &path, const std::uintmax_t maximum_bytes, const std::string_view description)
json read_json (const fs::path &path, const std::uintmax_t maximum_bytes, const std::string_view description)
std::string utc_now ()
void emit (const json &document)
std::string validation_details (const arachnespace::contracts::validation_result &result, const std::string_view description)
std::uint64_t unsigned_value (const json &object, const std::string_view key, const std::uint64_t default_value, const bool positive)
std::size_t size_value (const json &object, const std::string_view key, const std::size_t fallback)
const jsonrequired_object (const json &parent, const std::string_view key)
configuration load_configuration (const fs::path &input_path)
std::string policy_configuration_hash (const configuration &config, const std::string_view section)
fs::path command_path (const std::string &value)
bool path_is_in_protected_legacy (const fs::path &path, const configuration &config)
void atomic_write (const fs::path &destination, const std::string_view bytes, const bool replace)
void write_immutable_exact (const fs::path &destination, const std::string_view bytes, const std::string_view description)
ordered_json envelope_json (const arachne::coordination::envelope_record &envelope)
ordered_json snapshot_json (const arachne::penelope::snapshot_result &snapshot)
written_run_manifest write_graph_run_manifest (const configuration &config, const std::string_view domain_directory, const std::string_view graph_domain, const std::string_view run_id, ordered_json configuration_hashes, ordered_json inputs, const arachne::penelope::snapshot_result &snapshot)
ordered_json issue_json (std::string path, std::string message)
bool path_has_symlink (const fs::path &root, const fs::path &candidate)
fs::path resolve_plan_artifact (const configuration &config, const fs::path &control_path, const std::string_view storage_ref)
resolved_snapshot_export resolve_snapshot_export (const configuration &config, const fs::path &control_path, const arachnespace::contracts::contract_name expected_contract, const std::string_view expected_kind)
void verify_external_source_snapshot (const configuration &config, const json &external_graph)
void verify_product_coverage (const json &external_graph, const json &product_tables)
json parse_embedded_json (const json &row, const std::string_view key, json fallback)
json candidate_tables_for_viewer (const json &tables)
json materialize_jsonl_export (const fs::path &path, const bool candidate)
bool valid_logical_date (const std::string_view value)
arachne::ariadne::candidate_configuration candidate_configuration_from (const configuration &config)
int command_contract_validate (const options &arguments)
int command_intake (const options &arguments)
int command_cocoon_transition (const options &arguments)
ordered_json actor_inbox_issues (const arachne::coordination::operational_ledger &ledger, const fs::path &inbox)
int command_inbox_baseline (const options &arguments)
int command_inbox_verify (const options &arguments)
int command_product_inbox (const bool apply)
int command_product_rebuild_merge_hints ()
int command_candidate_rebuild (const options &arguments)
int command_fetch (const options &arguments)
std::string form_encode (const std::string_view value)
bool wikidata_entity_id (const std::string_view value)
std::string join_strings (const std::span< const std::string > values, const std::string_view separator)
ordered_json wikidata_point_fetch_request (const configuration &config, const json &plan, const json &planned, const std::span< const std::string > entities, const std::string &request_id)
ordered_json wikidata_bulk_fetch_request (const json &plan, const json &planned, std::string request_id, std::string locator)
int command_fetch_plan (const options &arguments)
int command_candidate_plan (const options &arguments)
int command_viewer_build (const options &arguments)
ordered_json capabilities ()
int dispatch (const std::vector< std::string > &arguments)

Variables

constexpr std::uintmax_t maximum_config_bytes = 4U * 1024U * 1024U
constexpr std::uintmax_t maximum_control_bytes = 64U * 1024U * 1024U
constexpr std::uintmax_t maximum_export_bytes = 1024ULL * 1024ULL * 1024ULL
std::atomic< std::uint64_t > temporary_sequence { 0 }

Typedef Documentation

◆ json

using anonymous_namespace{main.cpp}::json = nlohmann::json

Definition at line 45 of file main.cpp.

◆ ordered_json

using anonymous_namespace{main.cpp}::ordered_json = nlohmann::ordered_json

Definition at line 46 of file main.cpp.

Enumeration Type Documentation

◆ cocoon_status [1/2]

◆ cocoon_status [2/2]

Definition at line 14 of file coordinator.hpp.

Function Documentation

◆ actor_inbox_issues()

ordered_json anonymous_namespace{main.cpp}::actor_inbox_issues ( const arachne::coordination::operational_ledger & ledger,
const fs::path & inbox )
nodiscard

Definition at line 1205 of file main.cpp.

1208 {
1209 ordered_json issues = ordered_json::array();
1210 for (const auto& issue : ledger.verify_inbox(inbox)) {
1211 issues.push_back(
1212 issue_json(issue.path.generic_string(), issue.message)
1213 );
1214 }
1215 return issues;
1216}
std::vector< verification_issue > verify_inbox(const std::filesystem::path &inbox_root) const
nlohmann::ordered_json ordered_json
Definition main.cpp:46
ordered_json issue_json(std::string path, std::string message)
Definition main.cpp:693

◆ atomic_write()

void anonymous_namespace{main.cpp}::atomic_write ( const fs::path & destination,
const std::string_view bytes,
const bool replace )

Definition at line 487 of file main.cpp.

490 {
491 if (destination.empty() || !destination.has_filename()) {
492 throw cli_error("output path must identify a file");
493 }
494 fs::create_directories(destination.parent_path());
495 const std::uint64_t sequence
496 = temporary_sequence.fetch_add(1, std::memory_order_relaxed);
497 const fs::path staging = destination.parent_path()
498 / ("." + destination.filename().string() + ".tmp-"
499 + std::to_string(static_cast<long long>(::getpid())) + "-"
500 + std::to_string(sequence));
501 try {
502 {
503 std::ofstream output(staging, std::ios::binary | std::ios::trunc);
504 if (!output) {
505 throw cli_error(
506 "cannot create staging output: " + staging.string()
507 );
508 }
509 output.write(
510 bytes.data(), static_cast<std::streamsize>(bytes.size())
511 );
512 output.flush();
513 if (!output) {
514 throw cli_error(
515 "cannot write staging output: " + staging.string()
516 );
517 }
518 }
519 if (replace) {
520 std::error_code error;
521 fs::rename(staging, destination, error);
522 if (error) {
523 throw cli_error(
524 "cannot publish output " + destination.string() + ": "
525 + error.message()
526 );
527 }
528 } else {
529 fs::create_hard_link(staging, destination);
530 fs::remove(staging);
531 }
532 } catch (...) {
533 std::error_code ignored;
534 fs::remove(staging, ignored);
535 throw;
536 }
537}
std::atomic< std::uint64_t > temporary_sequence
Definition main.cpp:53

References temporary_sequence.

Referenced by write_graph_run_manifest().

Here is the caller graph for this function:

◆ candidate_configuration_from()

arachne::ariadne::candidate_configuration anonymous_namespace{main.cpp}::candidate_configuration_from ( const configuration & config)
nodiscard

Definition at line 1081 of file main.cpp.

1081 {
1082 const json& candidate
1083 = required_object(config.document, "candidate_rebuild");
1084 const json& sources = required_object(candidate, "sources");
1085 if (sources.empty()) {
1086 throw cli_error("candidate_rebuild.sources must not be empty");
1087 }
1088 const json& source = sources.begin().value();
1089 if (!source.is_object()) {
1090 throw cli_error("candidate source configuration must be an object");
1091 }
1092 arachne::ariadne::candidate_configuration result;
1093 result.pool_size = size_value(source, "candidate_pool_size", 3000U);
1094 result.target_size = size_value(source, "final_target", 1500U);
1095 result.group_count = size_value(source, "group_count", 4U);
1096
1097 const auto gray = source.find("gray_bonus_basis_points");
1098 if (gray != source.end()) {
1099 if (!gray->is_number_integer()) {
1100 throw cli_error("gray_bonus_basis_points must be an integer");
1101 }
1102 result.gray_bonus_basis_points = gray->get<int>();
1103 }
1104 const auto quality = source.find("quality_weight");
1105 if (quality != source.end()) {
1106 if (!quality->is_number()) {
1107 throw cli_error("quality_weight must be numeric");
1108 }
1109 result.quality_weight = quality->get<double>();
1110 }
1111 static_cast<void>(
1113 );
1114 return result;
1115}
static nlohmann::ordered_json configuration_values(const candidate_configuration &configuration={})
const json & required_object(const json &parent, const std::string_view key)
Definition main.cpp:313
std::size_t size_value(const json &object, const std::string_view key, const std::size_t fallback)
Definition main.cpp:281

References arachne::ariadne::candidate_configuration::gray_bonus_basis_points, and arachne::ariadne::candidate_configuration::quality_weight.

Referenced by command_candidate_plan().

Here is the caller graph for this function:

◆ candidate_tables_for_viewer()

json anonymous_namespace{main.cpp}::candidate_tables_for_viewer ( const json & tables)
nodiscard

Definition at line 916 of file main.cpp.

916 {
917 json result {
918 { "artifact_type", "research_candidate_graph_materialization_v1" },
919 { "format_version", 1 },
920 { "algorithm", { { "version", "candidate-materialization-v1" } } },
921 { "groups", json::array() },
922 { "candidates", json::array() },
923 { "works", json::array() },
924 { "relations", json::array() },
925 };
926 if (tables.contains("candidate_graph_info")
927 && tables.at("candidate_graph_info").is_array()
928 && !tables.at("candidate_graph_info").empty()) {
929 const auto& info = tables.at("candidate_graph_info").front();
930 result["algorithm"]["version"]
931 = info.value("algorithm_version", "candidate-materialization-v1");
932 }
933 for (const auto& group : tables.value("candidate_groups", json::array())) {
934 json materialized
935 = parse_embedded_json(group, "metadata_json", json::object());
936 materialized["group_id"] = group.value("id", "");
937 materialized["label"] = group.value("label", "");
938 materialized["order"] = group.value("ordinal", 0);
939 result["groups"].push_back(std::move(materialized));
940 }
941 for (const auto& node : tables.value("candidate_nodes", json::array())) {
942 const json source
943 = parse_embedded_json(node, "source_metadata_json", json::object());
944 const json reasons
945 = parse_embedded_json(node, "selection_reason_json", json::array());
946 const std::string kind = node.value("entity_type", "candidate");
947 if (kind == "candidate_work") {
948 json attributes { { "noncanonical", true },
949 { "soft_guidance", true } };
950 if (source.contains("year")) {
951 attributes["year"] = source.at("year");
952 }
953 result["works"].push_back(
954 { { "work_id", node.value("id", "") },
955 { "candidate_id", source.value("candidate_id", "") },
956 { "external_id", node.value("entity_ref", "") },
957 { "label", node.value("label", "") },
958 { "attributes", std::move(attributes) } }
959 );
960 continue;
961 }
962 result["candidates"].push_back(
963 { { "candidate_id", node.value("id", "") },
964 { "external_id", node.value("entity_ref", "") },
965 { "label", node.value("label", "") },
966 { "kind", kind },
967 { "rank", node.value("rank", 0) },
968 { "coverage", node.value("coverage", 0.0) },
969 { "group_id", node.value("group_id", "unassigned") },
970 { "selection_reasons", reasons },
971 { "attributes",
972 { { "noncanonical", true },
973 { "is_grey", node.value("is_grey", 0) != 0 },
974 { "source", source } } } }
975 );
976 }
977 for (const auto& edge : tables.value("candidate_edges", json::array())) {
978 result["relations"].push_back(
979 { { "relation_id", edge.value("id", "") },
980 { "source_id", edge.value("subject_id", "") },
981 { "target_id", edge.value("object_id", "") },
982 { "relation_type", edge.value("relation_type", "") },
983 { "weight", edge.value("weight", 0.0) },
984 { "provenance",
985 parse_embedded_json(edge, "metadata_json", json::object()) },
986 { "attributes", { { "soft_guidance", true } } } }
987 );
988 }
989 return result;
990}
json parse_embedded_json(const json &row, const std::string_view key, json fallback)
Definition main.cpp:896

◆ capabilities()

ordered_json anonymous_namespace{main.cpp}::capabilities ( )
nodiscard

Definition at line 2103 of file main.cpp.

2103 {
2104 return {
2105 { "format_version", 1 },
2106 { "commands",
2107 { "candidate-plan", "candidate-rebuild", "cocoon-transition",
2108 "contract-validate", "fetch", "fetch-plan-translate",
2109 "inbox-baseline", "inbox-verify", "intake",
2110 "product-check-inbox", "product-apply-inbox",
2111 "product-rebuild-merge-hints",
2112 "viewer-build" } },
2113 };
2114}

◆ command_candidate_plan()

int anonymous_namespace{main.cpp}::command_candidate_plan ( const options & arguments)

Definition at line 1951 of file main.cpp.

1951 {
1952 const configuration config
1953 = load_configuration(arguments.require("--config"));
1954 const fs::path external_graph_path
1955 = command_path(arguments.require("--external-graph"));
1956 const fs::path product_control_path
1957 = command_path(arguments.require("--product-snapshot"));
1958 const fs::path output_artifact
1959 = command_path(arguments.require("--output-artifact"));
1960 const fs::path output_control
1961 = command_path(arguments.require("--output-control"));
1963 output_artifact, config.artifact_store
1964 )) {
1965 throw cli_error(
1966 "candidate output artifact must be beneath the configured artifact "
1967 "store"
1968 );
1969 }
1970 if (arachne::coordination::path_is_within(output_control, config.queue)
1971 || path_is_in_protected_legacy(output_control, config)) {
1972 throw cli_error("candidate plan control must be outside the inbox");
1973 }
1974 const json external_graph = read_json(
1975 external_graph_path, maximum_export_bytes,
1976 "external candidate source graph"
1977 );
1978 if (!external_graph.is_object()
1979 || external_graph.value("artifact_type", "")
1980 != "external_candidate_source_graph_v1"
1981 || external_graph.value("format_version", 0) != 1) {
1982 throw cli_error(
1983 "external graph must be external_candidate_source_graph_v1"
1984 );
1985 }
1986 const resolved_snapshot_export product_snapshot = resolve_snapshot_export(
1987 config, product_control_path,
1989 "product-jsonl"
1990 );
1991 const json product_tables
1992 = materialize_jsonl_export(product_snapshot.export_path, false);
1993 verify_external_source_snapshot(config, external_graph);
1994 verify_product_coverage(external_graph, product_tables);
1995 const auto candidate_config = candidate_configuration_from(config);
1996 const ordered_json materialization
1998 external_graph, candidate_config
1999 );
2000 const fs::path storage_path
2001 = output_artifact.lexically_relative(config.artifact_store);
2002 if (storage_path.empty()
2004 storage_path.generic_string()
2005 )) {
2006 throw cli_error(
2007 "candidate output artifact has no safe artifact-store reference"
2008 );
2009 }
2010 const ordered_json control
2012 materialization, output_artifact, storage_path.generic_string(),
2013 product_snapshot.control.at("snapshot_id").get<std::string>(),
2014 product_snapshot.control.at("content_sha256").get<std::string>(),
2015 candidate_config, utc_now()
2016 );
2018 output_control, arachnespace::contracts::canonical_json(control) + "\n",
2019 true
2020 );
2021 emit(control);
2022 return 0;
2023}
const std::string & require(const std::string_view name) const
Definition main.cpp:226
static nlohmann::ordered_json write_plan(const nlohmann::json &materialization, const std::filesystem::path &destination, std::string storage_ref, std::string product_snapshot_id, std::string product_snapshot_sha256, const candidate_configuration &configuration, std::string created_at)
static nlohmann::ordered_json build(const nlohmann::json &external_graph, const candidate_configuration &configuration={})
void emit(const json &document)
Definition main.cpp:167
void atomic_write(const fs::path &destination, const std::string_view bytes, const bool replace)
Definition main.cpp:487
resolved_snapshot_export resolve_snapshot_export(const configuration &config, const fs::path &control_path, const arachnespace::contracts::contract_name expected_contract, const std::string_view expected_kind)
Definition main.cpp:762
void verify_external_source_snapshot(const configuration &config, const json &external_graph)
Definition main.cpp:836
arachne::ariadne::candidate_configuration candidate_configuration_from(const configuration &config)
Definition main.cpp:1081
constexpr std::uintmax_t maximum_export_bytes
Definition main.cpp:51
json materialize_jsonl_export(const fs::path &path, const bool candidate)
Definition main.cpp:993
bool path_is_in_protected_legacy(const fs::path &path, const configuration &config)
Definition main.cpp:479
configuration load_configuration(const fs::path &input_path)
Definition main.cpp:323
void verify_product_coverage(const json &external_graph, const json &product_tables)
Definition main.cpp:859
fs::path command_path(const std::string &value)
Definition main.cpp:474
json read_json(const fs::path &path, const std::uintmax_t maximum_bytes, const std::string_view description)
Definition main.cpp:142
bool path_is_within(const std::filesystem::path &path, const std::filesystem::path &possible_parent)
bool is_safe_relative_artifact_ref(std::string_view artifact_ref) noexcept
Definition crypto.cpp:249
std::string canonical_json(const nlohmann::json &document)

References candidate_configuration_from(), path_is_in_protected_legacy(), and utc_now().

Referenced by dispatch().

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

◆ command_candidate_rebuild()

int anonymous_namespace{main.cpp}::command_candidate_rebuild ( const options & arguments)

Definition at line 1354 of file main.cpp.

1354 {
1355 const configuration config
1356 = load_configuration(arguments.require("--config"));
1357 const fs::path control_path
1358 = command_path(arguments.require("--plan-control"));
1359 const json control = read_json(
1360 control_path, maximum_control_bytes, "candidate plan control"
1361 );
1362 const auto validation = arachnespace::contracts::validate(
1364 control
1365 );
1366 if (!validation) {
1367 throw cli_error(
1368 validation_details(validation, "candidate plan control")
1369 );
1370 }
1371 const fs::path payload = resolve_plan_artifact(
1372 config, control_path,
1373 control.at("plan_artifact").at("storage_ref").get<std::string>()
1374 );
1376 config, json { { "source_snapshot", control.at("source_snapshot") } }
1377 );
1378 const std::string product_snapshot_id
1379 = control.at("product_snapshot").at("snapshot_id").get<std::string>();
1380 const fs::path product_control_path = config.graph_store / "product"
1381 / "snapshots" / product_snapshot_id / "metadata.json";
1382 const resolved_snapshot_export product_snapshot = resolve_snapshot_export(
1383 config, product_control_path,
1385 "product-jsonl"
1386 );
1387 if (product_snapshot.control.at("snapshot_id") != product_snapshot_id
1388 || product_snapshot.control.at("content_sha256")
1389 != control.at("product_snapshot").at("sha256")) {
1390 throw cli_error(
1391 "candidate plan product input does not match the verified product "
1392 "snapshot"
1393 );
1394 }
1395 const std::string& run_id = arguments.require("--run-id");
1396 const std::string created_at = control.at("created_at").get<std::string>();
1397 if (created_at.size() < 10U
1398 || !valid_logical_date(std::string_view(created_at).substr(0, 10U))) {
1399 throw cli_error("candidate plan created_at has no valid logical date");
1400 }
1401 const std::string logical_date = created_at.substr(0, 10U);
1402 arachne::coordination::domain_lock lock(
1403 config.lock_root, "research_candidate_graph", run_id,
1405 );
1406 arachne::coordination::operational_ledger ledger(
1407 config.ledger, config.legacy_inbox
1408 );
1409 const std::string operations_hash
1410 = policy_configuration_hash(config, "candidate_rebuild");
1411 const std::string plan_control_hash
1412 = arachne::crypto::sha256_file(control_path);
1413 const std::string run_claim_hash = arachne::crypto::sha256(
1415 ordered_json { { "operations", operations_hash },
1416 { "plan_control", plan_control_hash } }
1417 )
1418 );
1419 if (!ledger.claim_logical_run(
1420 run_id, "research_candidate_graph", logical_date, run_claim_hash,
1421 true, true
1422 )) {
1423 emit(
1424 ordered_json {
1425 { "command", "candidate-rebuild" },
1426 { "run_id", run_id },
1427 { "processed", false },
1428 { "reason", "run_already_succeeded" },
1429 }
1430 );
1431 return 0;
1432 }
1433 std::optional<written_run_manifest> run_manifest;
1434 bool completed = false;
1435 try {
1436 arachne::penelope::store persistence(config.graph_store);
1437 const auto snapshot = persistence.replace_candidate_snapshot(
1438 { .run_id = run_id,
1439 .plan = { .control_contract_path = control_path,
1440 .resolved_plan_payload_path = payload } }
1441 );
1442 ordered_json inputs = ordered_json::array();
1443 inputs.push_back(
1444 { { "kind", "candidate-plan-control" },
1445 { "identity", control.at("plan_id") },
1446 { "sha256", plan_control_hash },
1447 { "byte_length", fs::file_size(control_path) } }
1448 );
1449 inputs.push_back(
1450 { { "kind", "candidate-plan-artifact" },
1451 { "identity", control.at("plan_id") },
1452 { "storage_ref", control.at("plan_artifact").at("storage_ref") },
1453 { "sha256", control.at("plan_artifact").at("sha256") },
1454 { "byte_length", control.at("plan_artifact").at("byte_length") } }
1455 );
1456 inputs.push_back(
1457 { { "kind", "external-source-snapshot" },
1458 { "identity", control.at("source_snapshot").at("snapshot_id") },
1459 { "storage_ref",
1460 control.at("source_snapshot").at("storage_ref") },
1461 { "sha256", control.at("source_snapshot").at("sha256") } }
1462 );
1463 inputs.push_back(
1464 { { "kind", "product-snapshot" },
1465 { "identity", product_snapshot_id },
1466 { "sha256", control.at("product_snapshot").at("sha256") } }
1467 );
1468 run_manifest = write_graph_run_manifest(
1469 config, "candidate", "research_candidate_graph", run_id,
1470 { { "operations", operations_hash },
1471 { "algorithm", control.at("configuration").at("sha256") } },
1472 std::move(inputs), snapshot
1473 );
1474 ledger.finish_run(run_id, "succeeded", run_manifest->storage_ref);
1475 completed = true;
1476 emit(
1477 ordered_json {
1478 { "command", "candidate-rebuild" },
1479 { "run_id", run_id },
1480 { "plan_id", control.at("plan_id") },
1481 { "snapshot", snapshot_json(snapshot) },
1482 { "run_manifest_path", run_manifest->path.generic_string() },
1483 { "run_manifest", run_manifest->document },
1484 }
1485 );
1486 return 0;
1487 } catch (...) {
1488 const std::exception_ptr failure = std::current_exception();
1489 if (completed) {
1490 std::rethrow_exception(failure);
1491 }
1492 try {
1493 ledger.finish_run(
1494 run_id, "failed",
1495 run_manifest ? std::string_view(run_manifest->storage_ref)
1496 : std::string_view {}
1497 );
1498 } catch (const std::exception& error) {
1499 std::cerr << "warning: cannot finish failed candidate run "
1500 << run_id << ": " << error.what() << '\n';
1501 }
1502 std::rethrow_exception(failure);
1503 }
1504}
std::string validation_details(const arachnespace::contracts::validation_result &result, const std::string_view description)
Definition main.cpp:171
written_run_manifest write_graph_run_manifest(const configuration &config, const std::string_view domain_directory, const std::string_view graph_domain, const std::string_view run_id, ordered_json configuration_hashes, ordered_json inputs, const arachne::penelope::snapshot_result &snapshot)
Definition main.cpp:601
ordered_json snapshot_json(const arachne::penelope::snapshot_result &snapshot)
Definition main.cpp:579
std::string policy_configuration_hash(const configuration &config, const std::string_view section)
Definition main.cpp:455
fs::path resolve_plan_artifact(const configuration &config, const fs::path &control_path, const std::string_view storage_ref)
Definition main.cpp:715
bool valid_logical_date(const std::string_view value)
Definition main.cpp:1056
constexpr std::uintmax_t maximum_control_bytes
Definition main.cpp:50
std::string sha256(std::span< const std::byte > bytes)
Definition crypto.cpp:209
std::string sha256_file(const std::filesystem::path &path)
Definition crypto.cpp:221
validation_result validate(const nlohmann::json &document)
std::optional< fs::path > legacy_inbox
Definition main.cpp:299
std::chrono::seconds candidate_lock_stale
Definition main.cpp:309

References arachne::penelope::store::replace_candidate_snapshot(), arachnespace::contracts::research_candidate_graph_plan, and arachne::crypto::sha256_file().

Referenced by dispatch().

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

◆ command_cocoon_transition()

int anonymous_namespace{main.cpp}::command_cocoon_transition ( const options & arguments)

Definition at line 1177 of file main.cpp.

1177 {
1178 const configuration config
1179 = load_configuration(arguments.require("--config"));
1181 config.ledger, config.legacy_inbox
1182 );
1184 arguments.require("--to")
1185 );
1186 std::string reason
1187 = arguments.optional("--reason")
1188 .value_or("transition requested through the operations CLI");
1189 if (reason.empty()) {
1190 reason = "transition requested through the operations CLI";
1191 }
1192 const auto envelope = ledger.transition(
1193 arguments.require("--envelope-id"), next,
1194 arguments.require("--actor-ref"), reason
1195 );
1196 emit(
1197 ordered_json {
1198 { "command", "cocoon-transition" },
1199 { "envelope", envelope_json(envelope) },
1200 }
1201 );
1202 return 0;
1203}
std::optional< std::string > optional(const std::string_view name) const
Definition main.cpp:235
ordered_json envelope_json(const arachne::coordination::envelope_record &envelope)
Definition main.cpp:560
cocoon_status cocoon_status_from_string(const std::string_view value)

Referenced by dispatch().

Here is the caller graph for this function:

◆ command_contract_validate()

int anonymous_namespace{main.cpp}::command_contract_validate ( const options & arguments)

Definition at line 1117 of file main.cpp.

1117 {
1118 static_cast<void>(load_configuration(arguments.require("--config")));
1119 const std::string& wire_name = arguments.require("--contract");
1120 const auto expected
1122 if (!expected) {
1123 throw cli_error("unsupported contract name: " + wire_name);
1124 }
1125 const fs::path input = command_path(arguments.require("--input"));
1126 const json document
1127 = read_json(input, maximum_control_bytes, "contract input");
1128 const auto validation
1129 = arachnespace::contracts::validate(*expected, document);
1130 ordered_json diagnostics = ordered_json::array();
1131 for (const auto& diagnostic : validation.diagnostics) {
1132 diagnostics.push_back(
1133 { { "instance_path", diagnostic.instance_path },
1134 { "code", diagnostic.code },
1135 { "message", diagnostic.message } }
1136 );
1137 }
1138 emit(
1139 ordered_json {
1140 { "command", "contract-validate" },
1141 { "contract", wire_name },
1142 { "input", input.generic_string() },
1143 { "valid", validation.valid() },
1144 { "diagnostics", std::move(diagnostics) },
1145 }
1146 );
1147 if (!validation.valid()) {
1148 std::cerr << validation_details(validation, wire_name) << '\n';
1149 return 3;
1150 }
1151 return 0;
1152}
std::optional< contract_name > parse_contract_name(std::string_view name) noexcept

Referenced by dispatch().

Here is the caller graph for this function:

◆ command_fetch()

int anonymous_namespace{main.cpp}::command_fetch ( const options & arguments)

Definition at line 1506 of file main.cpp.

1506 {
1507 const configuration config
1508 = load_configuration(arguments.require("--config"));
1509 const fs::path request_path = command_path(arguments.require("--request"));
1510 const fs::path output_control
1511 = command_path(arguments.require("--output-control"));
1512 if (arachne::coordination::path_is_within(output_control, config.queue)
1513 || path_is_in_protected_legacy(output_control, config)) {
1514 throw cli_error("acquired artifact control must be outside the inbox");
1515 }
1517 try {
1518 const json request
1519 = read_json(request_path, maximum_control_bytes, "fetch request");
1521 config.artifact_store, required_object(config.document, "transport")
1522 );
1523 acquired = transport.execute(request);
1524 } catch (const std::exception& error) {
1525 const auto now = std::chrono::system_clock::now();
1526 acquired.artifact_id = "artifact-invalid-fetch-request";
1527 acquired.request_id = "invalid-fetch-request";
1528 acquired.status
1530 acquired.source_url = "urn:arachne:invalid-fetch-request";
1531 acquired.started_at = now;
1532 acquired.completed_at = now;
1533 acquired.error_message = error.what();
1534 }
1535 const ordered_json control = arachne::pheidippides::to_contract(acquired);
1537 output_control, arachnespace::contracts::canonical_json(control) + "\n",
1538 true
1539 );
1540 emit(control);
1541 if (!acquired.delivered()) {
1542 std::cerr << "transport failed: " << acquired.error_message << '\n';
1543 return 3;
1544 }
1545 return 0;
1546}
nlohmann::ordered_json to_contract(const acquired_artifact_v1 &artifact)
std::chrono::system_clock::time_point completed_at
std::chrono::system_clock::time_point started_at

References arachne::pheidippides::acquired_artifact_v1::artifact_id, arachne::pheidippides::acquired_artifact_v1::delivered(), arachne::pheidippides::acquired_artifact_v1::error_message, arachne::pheidippides::hardened_transport::execute(), arachne::pheidippides::invalid_request, path_is_in_protected_legacy(), arachne::pheidippides::acquired_artifact_v1::request_id, arachne::pheidippides::acquired_artifact_v1::source_url, and arachne::pheidippides::acquired_artifact_v1::status.

Referenced by dispatch().

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

◆ command_fetch_plan()

int anonymous_namespace{main.cpp}::command_fetch_plan ( const options & arguments)

Definition at line 1782 of file main.cpp.

1782 {
1783 const configuration config
1784 = load_configuration(arguments.require("--config"));
1785 const fs::path plan_path = command_path(arguments.require("--plan"));
1786 const fs::path output_directory
1787 = command_path(arguments.require("--output-directory"));
1788 if (arachne::coordination::path_is_within(output_directory, config.queue)
1789 || path_is_in_protected_legacy(output_directory, config)) {
1790 throw cli_error("translated fetch controls must be outside the inbox");
1791 }
1792 const json plan = read_json(plan_path, maximum_control_bytes, "fetch plan");
1793 const auto validation = arachnespace::contracts::validate(
1795 );
1796 if (!validation) {
1797 throw cli_error(validation_details(validation, "fetch plan"));
1798 }
1799 if (plan.at("source") != "wikidata") {
1800 throw cli_error(
1801 "no closed fetch-plan adapter is registered for source "
1802 + plan.at("source").get<std::string>()
1803 );
1804 }
1805
1806 ordered_json generated = ordered_json::array();
1807 std::set<std::string, std::less<>> concrete_ids;
1808 for (const auto& planned : plan.at("requests")) {
1809 const bool has_entities = planned.contains("entities");
1810 const bool has_fields = planned.contains("fields");
1811 const bool has_pages = planned.contains("pages");
1812 const bool has_archives = planned.contains("archives");
1813 if (has_pages) {
1814 throw cli_error(
1815 "Wikidata page selectors are not supported by this adapter"
1816 );
1817 }
1818 if (has_entities || has_fields) {
1819 if (!has_entities || !planned.at("entities").is_array()
1820 || planned.at("entities").empty() || !has_fields
1821 || has_archives) {
1822 throw cli_error(
1823 "Wikidata point selectors require non-empty entities and "
1824 "fields only"
1825 );
1826 }
1827 if (planned.at("locator") != "https://www.wikidata.org/w/api.php") {
1828 throw cli_error(
1829 "Wikidata entity selector uses an unsupported locator"
1830 );
1831 }
1832 std::vector<std::string> entities;
1833 std::set<std::string, std::less<>> unique;
1834 for (const auto& entity : planned.at("entities")) {
1835 if (!entity.is_string()
1837 entity.get_ref<const std::string&>()
1838 )) {
1839 throw cli_error(
1840 "Wikidata entity selector contains an invalid ID"
1841 );
1842 }
1843 if (!unique.emplace(entity.get<std::string>()).second) {
1844 throw cli_error(
1845 "Wikidata entity selector contains a duplicate ID"
1846 );
1847 }
1848 entities.push_back(entity.get<std::string>());
1849 }
1850 constexpr std::size_t maximum_entities_per_request = 50U;
1851 const std::size_t part_count
1852 = (entities.size() - 1U) / maximum_entities_per_request + 1U;
1853 for (std::size_t part = 0; part < part_count; ++part) {
1854 const std::size_t begin = part * maximum_entities_per_request;
1855 const std::size_t count = std::min(
1856 maximum_entities_per_request, entities.size() - begin
1857 );
1858 std::string request_id
1859 = planned.at("request_id").get<std::string>();
1860 if (part_count != 1U) {
1861 request_id += "-part-" + std::to_string(part + 1U);
1862 }
1863 if (!concrete_ids.emplace(request_id).second) {
1864 throw cli_error(
1865 "fetch plan produces a duplicate request identity"
1866 );
1867 }
1868 generated.push_back(wikidata_point_fetch_request(
1869 config, plan, planned,
1870 std::span<const std::string>(entities).subspan(
1871 begin, count
1872 ),
1873 request_id
1874 ));
1875 }
1876 continue;
1877 }
1878 if (has_archives) {
1879 if (!planned.at("archives").is_array()
1880 || planned.at("archives").empty()) {
1881 throw cli_error("Wikidata archives selector must not be empty");
1882 }
1883 const std::string base = planned.at("locator").get<std::string>();
1884 if (!base.ends_with('/')) {
1885 throw cli_error(
1886 "Wikidata archive locator must end with a slash"
1887 );
1888 }
1889 std::size_t part = 0;
1890 for (const auto& archive : planned.at("archives")) {
1891 if (!archive.is_string()
1893 archive.get_ref<const std::string&>()
1894 )
1895 || archive.get_ref<const std::string&>().find('/')
1896 != std::string::npos) {
1897 throw cli_error(
1898 "Wikidata archive selector is not a safe filename"
1899 );
1900 }
1901 std::string request_id
1902 = planned.at("request_id").get<std::string>() + "-archive-"
1903 + std::to_string(++part);
1904 if (!concrete_ids.emplace(request_id).second) {
1905 throw cli_error(
1906 "fetch plan produces a duplicate request identity"
1907 );
1908 }
1909 generated.push_back(wikidata_bulk_fetch_request(
1910 plan, planned, std::move(request_id),
1911 base + archive.get<std::string>()
1912 ));
1913 }
1914 continue;
1915 }
1916 std::string request_id = planned.at("request_id").get<std::string>();
1917 if (!concrete_ids.emplace(request_id).second) {
1918 throw cli_error("fetch plan contains a duplicate request identity");
1919 }
1920 generated.push_back(wikidata_bulk_fetch_request(
1921 plan, planned, std::move(request_id),
1922 planned.at("locator").get<std::string>()
1923 ));
1924 }
1925
1926 ordered_json controls = ordered_json::array();
1927 for (const auto& request : generated) {
1928 const fs::path path = output_directory
1929 / (request.at("request_id").get<std::string>() + ".json");
1930 const std::string bytes
1932 write_immutable_exact(path, bytes, "translated fetch request");
1933 controls.push_back(
1934 { { "request_id", request.at("request_id") },
1935 { "path", path.generic_string() },
1936 { "sha256", arachne::crypto::sha256(bytes) },
1937 { "request", request } }
1938 );
1939 }
1940 emit(
1941 ordered_json {
1942 { "command", "fetch-plan-translate" },
1943 { "plan_id", plan.at("plan_id") },
1944 { "request_count", controls.size() },
1945 { "controls", std::move(controls) },
1946 }
1947 );
1948 return 0;
1949}
ordered_json wikidata_point_fetch_request(const configuration &config, const json &plan, const json &planned, const std::span< const std::string > entities, const std::string &request_id)
Definition main.cpp:1588
void write_immutable_exact(const fs::path &destination, const std::string_view bytes, const std::string_view description)
Definition main.cpp:539
ordered_json wikidata_bulk_fetch_request(const json &plan, const json &planned, std::string request_id, std::string locator)
Definition main.cpp:1698
bool wikidata_entity_id(const std::string_view value)
Definition main.cpp:1566

References arachnespace::contracts::fetch_plan, and path_is_in_protected_legacy().

Referenced by dispatch().

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

◆ command_inbox_baseline()

int anonymous_namespace{main.cpp}::command_inbox_baseline ( const options & arguments)

Definition at line 1218 of file main.cpp.

1218 {
1219 const configuration config
1220 = load_configuration(arguments.require("--config"));
1221 if (!config.legacy_inbox || !fs::is_directory(*config.legacy_inbox)) {
1222 throw cli_error(
1223 "an existing paths.legacy_inbox directory is required for legacy "
1224 "baseline operations"
1225 );
1226 }
1228 config.ledger, config.legacy_inbox
1229 );
1230 ordered_json issues = actor_inbox_issues(ledger, *config.legacy_inbox);
1231 if (!issues.empty()) {
1232 emit(
1233 ordered_json {
1234 { "command", "inbox-baseline" },
1235 { "ok", false },
1236 { "issues", issues },
1237 }
1238 );
1239 std::cerr << "refusing to replace an inbox baseline after mutation\n";
1240 return 3;
1241 }
1242 ledger.capture_inbox_baseline(*config.legacy_inbox);
1243 issues = actor_inbox_issues(ledger, *config.legacy_inbox);
1244 if (!issues.empty()) {
1245 emit(
1246 ordered_json {
1247 { "command", "inbox-baseline" },
1248 { "ok", false },
1249 { "issues", issues },
1250 }
1251 );
1252 std::cerr << "inbox changed while its baseline was being captured\n";
1253 return 3;
1254 }
1255 emit(
1256 ordered_json {
1257 { "command", "inbox-baseline" },
1258 { "ok", true },
1259 { "legacy_inbox", config.legacy_inbox->generic_string() },
1260 }
1261 );
1262 return 0;
1263}
ordered_json actor_inbox_issues(const arachne::coordination::operational_ledger &ledger, const fs::path &inbox)
Definition main.cpp:1205

References arachne::coordination::operational_ledger::capture_inbox_baseline().

Referenced by dispatch().

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

◆ command_inbox_verify()

int anonymous_namespace{main.cpp}::command_inbox_verify ( const options & arguments)

Definition at line 1265 of file main.cpp.

1265 {
1266 const configuration config
1267 = load_configuration(arguments.require("--config"));
1268 if (!config.legacy_inbox || !fs::is_directory(*config.legacy_inbox)) {
1269 throw cli_error(
1270 "an existing paths.legacy_inbox directory is required for legacy "
1271 "verification operations"
1272 );
1273 }
1275 config.ledger, config.legacy_inbox
1276 );
1277 ordered_json issues = actor_inbox_issues(ledger, *config.legacy_inbox);
1278 const bool ok = issues.empty();
1279 emit(
1280 ordered_json {
1281 { "command", "inbox-verify" },
1282 { "ok", ok },
1283 { "legacy_inbox", config.legacy_inbox->generic_string() },
1284 { "issues", issues },
1285 }
1286 );
1287 if (!ok) {
1288 for (const auto& issue : issues) {
1289 std::cerr << issue.at("path").get<std::string>() << ": "
1290 << issue.at("message").get<std::string>() << '\n';
1291 }
1292 return 3;
1293 }
1294 return 0;
1295}

Referenced by dispatch().

Here is the caller graph for this function:

◆ command_intake()

int anonymous_namespace{main.cpp}::command_intake ( const options & arguments)

Definition at line 1154 of file main.cpp.

1154 {
1155 const configuration config
1156 = load_configuration(arguments.require("--config"));
1158 config.ledger, config.legacy_inbox
1159 );
1160 const fs::path payload = command_path(arguments.require("--payload"));
1161 const auto state = fs::symlink_status(payload);
1162 if (fs::is_symlink(state) || !fs::is_regular_file(state)) {
1163 throw cli_error("intake payload must be a non-symlink regular file");
1164 }
1165 static_cast<void>(ledger.intake(
1166 { .source_path = payload,
1167 .inbox_root = config.queue,
1168 .submission_ref = arguments.require("--submission-ref"),
1169 .title = arguments.require("--title"),
1170 .supersedes = arguments.optional("--supersedes"),
1171 .max_payload_bytes = config.submission_max_bytes }
1172 ));
1173 emit(ordered_json { { "status", "ok" } });
1174 return 0;
1175}

References arachne::coordination::operational_ledger::intake(), and anonymous_namespace{main.cpp}::configuration::submission_max_bytes.

Referenced by dispatch().

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

◆ command_path()

fs::path anonymous_namespace{main.cpp}::command_path ( const std::string & value)
nodiscard

Definition at line 474 of file main.cpp.

474 {
475 return resolved_path(fs::path(value), repository_root());
476}
fs::path resolved_path(const fs::path &path, const fs::path &relative_root)
Definition main.cpp:76

References repository_root(), and resolved_path().

Here is the call graph for this function:

◆ command_product_inbox()

int anonymous_namespace{main.cpp}::command_product_inbox ( const bool apply)

Definition at line 1297 of file main.cpp.

1297 {
1298 const auto result = apply
1300 : arachne::penelope::check_product_inbox(repository_root());
1301 ordered_json batches = ordered_json::array();
1302 for (const auto& batch : result.batches) {
1303 ordered_json issues = ordered_json::array();
1304 for (const auto& issue : batch.issues) {
1305 ordered_json item {
1306 { "code", issue.code },
1307 { "json_path", issue.json_path },
1308 { "message", issue.message },
1309 };
1310 if (!issue.value_json.empty()) {
1311 item["value"] = json::parse(issue.value_json);
1312 }
1313 issues.push_back(std::move(item));
1314 }
1315 batches.push_back(
1316 ordered_json {
1317 { "path",
1318 batch.path.lexically_relative(repository_root())
1319 .generic_string() },
1320 { "batch_id", batch.batch_id },
1321 { "status", arachne::penelope::to_string(batch.status) },
1322 { "issues", std::move(issues) },
1323 }
1324 );
1325 }
1326 emit(
1327 ordered_json {
1328 { "status", result.ok ? "ok" : "fail" },
1329 { "command",
1330 apply ? "product-apply-inbox" : "product-check-inbox" },
1331 { "valid_count", result.valid_count },
1332 { "applied_count", result.applied_count },
1333 { "already_applied_count", result.already_applied_count },
1334 { "rejected_count", result.rejected_count },
1335 { "batches", std::move(batches) },
1336 }
1337 );
1338 return result.ok ? 0 : 3;
1339}
inbox_result apply_product_inbox(const std::filesystem::path &repository_root)
const char * to_string(inbox_batch_status status) noexcept
Definition inbox.cpp:6133
@ item
IDs prefixed with 'Q'.
Definition utils.hpp:48

References arachne::penelope::apply_product_inbox(), arachne::penelope::check_product_inbox(), arachne::penelope::inbox_result::ok, and repository_root().

Referenced by dispatch().

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

◆ command_product_rebuild_merge_hints()

int anonymous_namespace{main.cpp}::command_product_rebuild_merge_hints ( )

Definition at line 1341 of file main.cpp.

1341 {
1342 const std::size_t count
1344 emit(
1345 ordered_json {
1346 { "status", "ok" },
1347 { "command", "product-rebuild-merge-hints" },
1348 { "candidate_count", count },
1349 }
1350 );
1351 return 0;
1352}
std::size_t rebuild_product_merge_hints(const std::filesystem::path &repository_root)

References arachne::penelope::rebuild_product_merge_hints(), and repository_root().

Referenced by dispatch().

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

◆ command_viewer_build()

int anonymous_namespace{main.cpp}::command_viewer_build ( const options & arguments)

Definition at line 2025 of file main.cpp.

2025 {
2026 const configuration config
2027 = load_configuration(arguments.require("--config"));
2028 const auto candidate_control = arguments.optional("--candidate-snapshot");
2029 if (!fs::is_directory(config.viewer_templates)) {
2030 throw cli_error("configured viewer template directory does not exist");
2031 }
2032 const resolved_snapshot_export product_snapshot = resolve_snapshot_export(
2033 config, command_path(arguments.require("--product-snapshot")),
2035 "product-jsonl"
2036 );
2037 const json product
2038 = materialize_jsonl_export(product_snapshot.export_path, false);
2039 json candidate = json::object();
2040 std::string candidate_id = "none";
2041 if (candidate_control) {
2042 const resolved_snapshot_export candidate_snapshot
2044 config, command_path(*candidate_control),
2046 research_candidate_graph_snapshot,
2047 "candidate-jsonl"
2048 );
2049 candidate
2050 = materialize_jsonl_export(candidate_snapshot.export_path, true);
2051 candidate_id
2052 = candidate_snapshot.control.at("snapshot_id").get<std::string>();
2053 }
2055 product, candidate,
2056 product_snapshot.control.at("snapshot_id").get<std::string>(),
2057 candidate_id
2058 );
2060 product,
2061 product_snapshot.control.at("snapshot_id").get<std::string>()
2062 );
2063 const std::string projection_id
2064 = projection.at("projection_id").get<std::string>();
2065 const fs::path projection_directory = config.site_output / "projections";
2066 const fs::path projection_path
2067 = projection_directory / (projection_id + ".json");
2068 const fs::path control_path
2069 = projection_directory / (projection_id + ".control.json");
2070 const std::string generated_at = utc_now();
2071 const std::string settings_hash = arachne::crypto::sha256(
2073 required_object(config.document, "publication")
2074 )
2075 );
2076 const fs::path storage_ref
2077 = projection_path.lexically_relative(config.site_output);
2078 const ordered_json projection_control
2080 projection, projection_path, storage_ref.generic_string(),
2081 settings_hash, generated_at
2082 );
2084 control_path,
2085 arachnespace::contracts::canonical_json(projection_control) + "\n", true
2086 );
2089 projection, catalog, config.viewer_templates, config.site_output,
2090 generated_at
2091 );
2092 emit(
2093 ordered_json {
2094 { "command", "viewer-build" },
2095 { "projection_control", projection_control },
2096 { "projection_control_path", control_path.generic_string() },
2097 { "site_bundle", site_bundle },
2098 }
2099 );
2100 return 0;
2101}
static nlohmann::ordered_json build_site(const nlohmann::json &projection, const nlohmann::json &catalog_data, const std::filesystem::path &template_root, const std::filesystem::path &site_root, std::string generated_at)
Definition viewer.cpp:1233
static nlohmann::ordered_json catalog(const nlohmann::json &product_export, std::string product_snapshot_id)
Definition viewer.cpp:901
static nlohmann::ordered_json project(const nlohmann::json &product_export, const nlohmann::json &candidate_export, std::string product_snapshot_id, std::string candidate_snapshot_id="none")
Definition viewer.cpp:305
static nlohmann::ordered_json write_projection(const nlohmann::json &projection_data, const std::filesystem::path &destination, std::string storage_ref, std::string settings_sha256, std::string generated_at)
Definition viewer.cpp:1186

References utc_now().

Referenced by dispatch().

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

◆ conventional_legacy_inbox()

std::optional< fs::path > anonymous_namespace{main.cpp}::conventional_legacy_inbox ( )
nodiscard

Definition at line 91 of file main.cpp.

91 {
92 const char* home = std::getenv("HOME");
93 if (home == nullptr || *home == '\0') {
94 return std::nullopt;
95 }
96 return resolved_path(
97 fs::path(home) / "Projects/new/art-lineages/inbox", repository_root()
98 );
99}

References repository_root().

Here is the call graph for this function:

◆ dispatch()

int anonymous_namespace{main.cpp}::dispatch ( const std::vector< std::string > & arguments)

Definition at line 2116 of file main.cpp.

2116 {
2117 if (arguments.size() == 2U && arguments[1] == "--capabilities-json") {
2118 emit(capabilities());
2119 return 0;
2120 }
2121 if (arguments.size() < 2U) {
2122 throw cli_error("a command is required");
2123 }
2124
2125 if (arguments[1] == "contract" && arguments.size() >= 3U
2126 && arguments[2] == "validate") {
2128 options(arguments, 3U, { "--config", "--contract", "--input" })
2129 );
2130 }
2131 if (arguments[1] == "intake") {
2132 return command_intake(options(
2133 arguments, 2U,
2134 { "--config", "--payload", "--submission-ref", "--title",
2135 "--supersedes" }
2136 ));
2137 }
2138 if (arguments[1] == "cocoon" && arguments.size() >= 3U
2139 && arguments[2] == "transition") {
2141 arguments, 3U,
2142 { "--config", "--envelope-id", "--to", "--actor-ref", "--reason" }
2143 ));
2144 }
2145 if (arguments[1] == "inbox" && arguments.size() >= 3U
2146 && arguments[2] == "baseline") {
2147 return command_inbox_baseline(options(arguments, 3U, { "--config" }));
2148 }
2149 if (arguments[1] == "inbox" && arguments.size() >= 3U
2150 && arguments[2] == "verify") {
2151 return command_inbox_verify(options(arguments, 3U, { "--config" }));
2152 }
2153 if (arguments[1] == "product" && arguments.size() >= 3U
2154 && arguments[2] == "check-inbox") {
2155 static_cast<void>(options(arguments, 3U, {}));
2156 return command_product_inbox(false);
2157 }
2158 if (arguments[1] == "product" && arguments.size() >= 3U
2159 && arguments[2] == "apply-inbox") {
2160 static_cast<void>(options(arguments, 3U, {}));
2161 return command_product_inbox(true);
2162 }
2163 if (arguments[1] == "product" && arguments.size() >= 3U
2164 && arguments[2] == "rebuild-merge-hints") {
2165 static_cast<void>(options(arguments, 3U, {}));
2167 }
2168 if (arguments[1] == "candidate" && arguments.size() >= 3U
2169 && arguments[2] == "rebuild") {
2171 options(arguments, 3U, { "--config", "--plan-control", "--run-id" })
2172 );
2173 }
2174 if (arguments[1] == "candidate" && arguments.size() >= 3U
2175 && arguments[2] == "plan") {
2177 arguments, 3U,
2178 { "--config", "--external-graph", "--product-snapshot",
2179 "--output-artifact", "--output-control" }
2180 ));
2181 }
2182 if (arguments[1] == "fetch" && arguments.size() >= 3U
2183 && arguments[2] == "plan") {
2185 arguments, 3U, { "--config", "--plan", "--output-directory" }
2186 ));
2187 }
2188 if (arguments[1] == "fetch") {
2189 return command_fetch(options(
2190 arguments, 2U, { "--config", "--request", "--output-control" }
2191 ));
2192 }
2193 if (arguments[1] == "viewer" && arguments.size() >= 3U
2194 && arguments[2] == "build") {
2196 arguments, 3U,
2197 { "--config", "--product-snapshot", "--candidate-snapshot" }
2198 ));
2199 }
2200 throw cli_error("unknown operations command");
2201}
int command_inbox_baseline(const options &arguments)
Definition main.cpp:1218
int command_fetch(const options &arguments)
Definition main.cpp:1506
int command_contract_validate(const options &arguments)
Definition main.cpp:1117
int command_candidate_rebuild(const options &arguments)
Definition main.cpp:1354
int command_fetch_plan(const options &arguments)
Definition main.cpp:1782
int command_intake(const options &arguments)
Definition main.cpp:1154
int command_cocoon_transition(const options &arguments)
Definition main.cpp:1177
ordered_json capabilities()
Definition main.cpp:2103
int command_candidate_plan(const options &arguments)
Definition main.cpp:1951
int command_viewer_build(const options &arguments)
Definition main.cpp:2025
int command_inbox_verify(const options &arguments)
Definition main.cpp:1265
int command_product_inbox(const bool apply)
Definition main.cpp:1297

References command_candidate_plan(), command_candidate_rebuild(), command_cocoon_transition(), command_contract_validate(), command_fetch(), command_fetch_plan(), command_inbox_baseline(), command_inbox_verify(), command_intake(), command_product_inbox(), command_product_rebuild_merge_hints(), and command_viewer_build().

Here is the call graph for this function:

◆ emit()

void anonymous_namespace{main.cpp}::emit ( const json & document)

Definition at line 167 of file main.cpp.

167 {
168 std::cout << arachnespace::contracts::canonical_json(document) << '\n';
169}

◆ envelope_json()

ordered_json anonymous_namespace{main.cpp}::envelope_json ( const arachne::coordination::envelope_record & envelope)
nodiscard

Definition at line 560 of file main.cpp.

560 {
561 ordered_json result {
562 { "envelope_id", envelope.envelope_id },
563 { "payload_ref", envelope.payload_ref.generic_string() },
564 { "payload_sha256", envelope.payload_sha256 },
565 { "byte_length", envelope.byte_length },
566 { "format_version", envelope.format_version },
567 { "submission_ref", envelope.submission_ref },
568 { "title", envelope.title },
569 { "status", arachne::coordination::to_string(envelope.status) },
570 };
571 result["accepted_by"]
572 = envelope.accepted_by ? json(*envelope.accepted_by) : json(nullptr);
573 result["supersedes"]
574 = envelope.supersedes ? json(*envelope.supersedes) : json(nullptr);
575 return result;
576}
std::string_view to_string(const cocoon_status status) noexcept
std::optional< std::string > supersedes
std::optional< std::string > accepted_by

References arachne::coordination::envelope_record::byte_length, arachne::coordination::envelope_record::envelope_id, arachne::coordination::envelope_record::format_version, arachne::coordination::envelope_record::payload_sha256, arachne::coordination::envelope_record::status, arachne::coordination::envelope_record::submission_ref, and arachne::coordination::envelope_record::title.

◆ form_encode()

std::string anonymous_namespace{main.cpp}::form_encode ( const std::string_view value)
nodiscard

Definition at line 1548 of file main.cpp.

1548 {
1549 constexpr std::string_view hexadecimal = "0123456789ABCDEF";
1550 std::string result;
1551 result.reserve(value.size());
1552 for (const char raw_character : value) {
1553 const auto character = static_cast<unsigned char>(raw_character);
1554 if (std::isalnum(character) != 0 || character == '-' || character == '_'
1555 || character == '.' || character == '~') {
1556 result.push_back(static_cast<char>(character));
1557 } else {
1558 result.push_back('%');
1559 result.push_back(hexadecimal.at(character >> 4U));
1560 result.push_back(hexadecimal.at(character & 0x0fU));
1561 }
1562 }
1563 return result;
1564}

◆ issue_json()

ordered_json anonymous_namespace{main.cpp}::issue_json ( std::string path,
std::string message )
nodiscard

Definition at line 693 of file main.cpp.

693 {
694 return { { "path", std::move(path) }, { "message", std::move(message) } };
695}

◆ join_strings()

std::string anonymous_namespace{main.cpp}::join_strings ( const std::span< const std::string > values,
const std::string_view separator )
nodiscard

Definition at line 1575 of file main.cpp.

1577 {
1578 std::string result;
1579 for (std::size_t index = 0; index < values.size(); ++index) {
1580 if (index != 0U) {
1581 result += separator;
1582 }
1583 result += values[index];
1584 }
1585 return result;
1586}

◆ load_configuration()

configuration anonymous_namespace{main.cpp}::load_configuration ( const fs::path & input_path)
nodiscard

Definition at line 323 of file main.cpp.

323 {
324 configuration result;
325 result.file = resolved_path(input_path, repository_root());
326 result.document = read_json(
327 result.file, maximum_config_bytes, "operations configuration"
328 );
329 if (!result.document.is_object()
330 || result.document.value("format_version", 0) != 1) {
331 throw cli_error("only operations configuration version 1 is supported");
332 }
333 if (!result.document.contains("project_timezone")
334 || !result.document.at("project_timezone").is_string()
335 || result.document.at("project_timezone")
336 .get_ref<const std::string&>()
337 .empty()) {
338 throw cli_error("project_timezone must be a non-empty string");
339 }
340
341 const json& paths = required_object(result.document, "paths");
342 const fs::path root = repository_root();
343 auto configured_path = [&](const std::string_view key) {
344 const auto value = paths.find(key);
345 if (value == paths.end() || !value->is_string()
346 || value->get_ref<const std::string&>().empty()) {
347 throw cli_error(
348 "configuration paths." + std::string(key) + " is missing"
349 );
350 }
351 const std::string& text = value->get_ref<const std::string&>();
352 if (text.starts_with("/absolute/path/to/")) {
353 throw cli_error(
354 "replace the placeholder paths." + std::string(key)
355 );
356 }
357 return resolved_path(fs::path(text), root);
358 };
359 result.queue = configured_path("queue");
360 if (const auto legacy = paths.find("legacy_inbox");
361 legacy != paths.end() && !legacy->is_null()) {
362 if (!legacy->is_string()
363 || legacy->get_ref<const std::string&>().empty()) {
364 throw cli_error(
365 "configuration paths.legacy_inbox must be a non-empty string"
366 );
367 }
369 fs::path(legacy->get_ref<const std::string&>()), root
370 );
371 }
372 result.ledger = configured_path("ledger");
373 result.graph_store = configured_path("graph_store");
374 result.artifact_store = configured_path("artifact_store");
375 result.lock_root = configured_path("lock_root");
376 result.viewer_templates = configured_path("viewer_templates");
377 result.site_output = configured_path("site_output");
378 if (!fs::is_directory(result.queue)) {
379 throw cli_error(
380 "configured queue is not a directory: " + result.queue.string()
381 );
382 }
383 if (const auto conventional = conventional_legacy_inbox()) {
384 result.protected_legacy_inboxes.push_back(*conventional);
385 }
386 if (result.legacy_inbox
387 && std::ranges::find(
389 ) == result.protected_legacy_inboxes.end()) {
390 result.protected_legacy_inboxes.push_back(*result.legacy_inbox);
391 }
392 for (const auto& legacy : result.protected_legacy_inboxes) {
394 || arachne::coordination::path_is_within(legacy, result.queue)) {
395 throw cli_error(
396 "the mutable queue and read-only legacy inbox must be disjoint"
397 );
398 }
399 }
400
401 for (const auto& [name, path] :
402 std::array<std::pair<std::string_view, const fs::path*>, 6> {
403 { { "ledger", &result.ledger },
404 { "graph_store", &result.graph_store },
405 { "artifact_store", &result.artifact_store },
406 { "lock_root", &result.lock_root },
407 { "viewer_templates", &result.viewer_templates },
408 { "site_output", &result.site_output } } }) {
411 throw cli_error(
412 "configuration paths." + std::string(name)
413 + " and the mutable batch queue must be disjoint"
414 );
415 }
416 for (const auto& legacy : result.protected_legacy_inboxes) {
418 || arachne::coordination::path_is_within(legacy, *path)) {
419 throw cli_error(
420 "configuration paths." + std::string(name)
421 + " and the read-only legacy inbox must be disjoint"
422 );
423 }
424 }
425 }
426
427 const json& security = required_object(result.document, "security");
429 security, "submission_max_bytes", 64U * 1024U * 1024U, true
430 );
431 const json& product
432 = required_object(result.document, "product_integration");
433 const json& candidate
434 = required_object(result.document, "candidate_rebuild");
435 const std::uint64_t product_stale
436 = unsigned_value(product, "lock_stale_seconds", 21'600U, true);
437 const std::uint64_t candidate_stale
438 = unsigned_value(candidate, "lock_stale_seconds", 21'600U, true);
439 const auto seconds_max = static_cast<std::uint64_t>(
440 std::numeric_limits<std::chrono::seconds::rep>::max()
441 );
442 if (product_stale > seconds_max || candidate_stale > seconds_max) {
443 throw cli_error("configured lock stale interval is too large");
444 }
445 result.product_lock_stale = std::chrono::seconds(
446 static_cast<std::chrono::seconds::rep>(product_stale)
447 );
448 result.candidate_lock_stale = std::chrono::seconds(
449 static_cast<std::chrono::seconds::rep>(candidate_stale)
450 );
451 static_cast<void>(required_object(result.document, "publication"));
452 return result;
453}
constexpr std::uintmax_t maximum_config_bytes
Definition main.cpp:49
std::optional< fs::path > conventional_legacy_inbox()
Definition main.cpp:91
std::uint64_t unsigned_value(const json &object, const std::string_view key, const std::uint64_t default_value, const bool positive)
Definition main.cpp:252
std::vector< fs::path > protected_legacy_inboxes
Definition main.cpp:300

References repository_root(), and resolved_path().

Here is the call graph for this function:

◆ materialize_jsonl_export()

json anonymous_namespace{main.cpp}::materialize_jsonl_export ( const fs::path & path,
const bool candidate )
nodiscard

Definition at line 993 of file main.cpp.

993 {
994 const std::string bytes
995 = read_bytes(path, maximum_export_bytes, "graph export");
996 const json whole = json::parse(bytes, nullptr, false);
997 if (!whole.is_discarded()) {
998 if (whole.is_object() && !whole.contains("table")) {
999 if (candidate && whole.contains("candidate_nodes")) {
1000 return candidate_tables_for_viewer(whole);
1001 }
1002 return whole;
1003 }
1004 if (!whole.is_array() && !whole.is_object()) {
1005 throw cli_error(
1006 "graph export JSON must be an object or record array"
1007 );
1008 }
1009 }
1010
1011 json tables = json::object();
1012 auto add_record = [&](const json& record, const std::size_t line_number) {
1013 if (!record.is_object() || !record.contains("table")
1014 || !record.at("table").is_string() || !record.contains("row")
1015 || !record.at("row").is_object()) {
1016 throw cli_error(
1017 "invalid Penelope JSONL record at line "
1018 + std::to_string(line_number)
1019 );
1020 }
1021 const std::string table = record.at("table").get<std::string>();
1022 if (!tables.contains(table)) {
1023 tables[table] = json::array();
1024 }
1025 tables[table].push_back(record.at("row"));
1026 };
1027 if (whole.is_array()) {
1028 std::size_t index = 0;
1029 for (const auto& record : whole) {
1030 add_record(record, ++index);
1031 }
1032 } else if (whole.is_object()) {
1033 add_record(whole, 1U);
1034 } else {
1035 std::istringstream lines(bytes);
1036 std::string line;
1037 std::size_t line_number = 0;
1038 while (std::getline(lines, line)) {
1039 ++line_number;
1040 if (line.empty()) {
1041 continue;
1042 }
1043 try {
1044 add_record(json::parse(line), line_number);
1045 } catch (const json::exception& error) {
1046 throw cli_error(
1047 "invalid graph JSONL at line " + std::to_string(line_number)
1048 + ": " + error.what()
1049 );
1050 }
1051 }
1052 }
1053 return candidate ? candidate_tables_for_viewer(tables) : tables;
1054}
json candidate_tables_for_viewer(const json &tables)
Definition main.cpp:916
std::string read_bytes(const fs::path &path, const std::uintmax_t maximum_bytes, const std::string_view description)
Definition main.cpp:101

◆ parse_embedded_json()

json anonymous_namespace{main.cpp}::parse_embedded_json ( const json & row,
const std::string_view key,
json fallback )
nodiscard

Definition at line 896 of file main.cpp.

898 {
899 const auto value = row.find(key);
900 if (value == row.end() || value->is_null()) {
901 return fallback;
902 }
903 if (!value->is_string()) {
904 return *value;
905 }
906 try {
907 return json::parse(value->get_ref<const std::string&>());
908 } catch (const json::exception& error) {
909 throw cli_error(
910 "invalid embedded JSON in export field " + std::string(key) + ": "
911 + error.what()
912 );
913 }
914}

◆ path_has_symlink()

bool anonymous_namespace{main.cpp}::path_has_symlink ( const fs::path & root,
const fs::path & candidate )
nodiscard

Definition at line 698 of file main.cpp.

698 {
699 const fs::path relative = candidate.lexically_relative(root);
700 fs::path current = root;
701 for (const auto& component : relative) {
702 current /= component;
703 std::error_code error;
704 const auto state = fs::symlink_status(current, error);
705 if (error) {
706 return true;
707 }
708 if (fs::is_symlink(state)) {
709 return true;
710 }
711 }
712 return false;
713}

◆ path_is_in_protected_legacy()

bool anonymous_namespace{main.cpp}::path_is_in_protected_legacy ( const fs::path & path,
const configuration & config )
nodiscard

Definition at line 479 of file main.cpp.

479 {
480 return std::ranges::any_of(
481 config.protected_legacy_inboxes, [&](const fs::path& legacy) {
482 return arachne::coordination::path_is_within(path, legacy);
483 }
484 );
485}

Referenced by command_candidate_plan(), command_fetch(), and command_fetch_plan().

Here is the caller graph for this function:

◆ policy_configuration_hash()

std::string anonymous_namespace{main.cpp}::policy_configuration_hash ( const configuration & config,
const std::string_view section )
nodiscard

Definition at line 455 of file main.cpp.

457 {
458 const auto value = config.document.find(section);
459 if (value == config.document.end()) {
460 throw cli_error(
461 "configuration section is missing: " + std::string(section)
462 );
463 }
464 const ordered_json stable {
465 { "format_version", config.document.at("format_version") },
466 { "project_timezone", config.document.at("project_timezone") },
467 { std::string(section), *value },
468 };
471 );
472}

References arachne::crypto::sha256().

Here is the call graph for this function:

◆ read_bytes()

std::string anonymous_namespace{main.cpp}::read_bytes ( const fs::path & path,
const std::uintmax_t maximum_bytes,
const std::string_view description )
nodiscard

Definition at line 101 of file main.cpp.

104 {
105 std::error_code error;
106 const auto state = fs::symlink_status(path, error);
107 if (error || !fs::is_regular_file(state)) {
108 throw cli_error(
109 std::string(description)
110 + " is not a regular file: " + path.string()
111 );
112 }
113 const std::uintmax_t size = fs::file_size(path, error);
114 if (error) {
115 throw cli_error(
116 "cannot inspect " + std::string(description) + ": "
117 + error.message()
118 );
119 }
120 if (size > maximum_bytes
121 || size > static_cast<std::uintmax_t>(
122 std::numeric_limits<std::streamsize>::max()
123 )) {
124 throw cli_error(std::string(description) + " exceeds its byte limit");
125 }
126 std::ifstream input(path, std::ios::binary);
127 if (!input) {
128 throw cli_error(
129 "cannot open " + std::string(description) + ": " + path.string()
130 );
131 }
132 std::string result(static_cast<std::size_t>(size), '\0');
133 if (!result.empty()) {
134 input.read(result.data(), static_cast<std::streamsize>(result.size()));
135 }
136 if (!input || input.peek() != std::char_traits<char>::eof()) {
137 throw cli_error("cannot read complete " + std::string(description));
138 }
139 return result;
140}

Referenced by write_immutable_exact().

Here is the caller graph for this function:

◆ read_json()

json anonymous_namespace{main.cpp}::read_json ( const fs::path & path,
const std::uintmax_t maximum_bytes,
const std::string_view description )
nodiscard

Definition at line 142 of file main.cpp.

145 {
146 const std::string bytes = read_bytes(path, maximum_bytes, description);
147 try {
148 return json::parse(bytes);
149 } catch (const json::exception& error) {
150 throw cli_error(
151 std::string(description) + " is not valid JSON: " + error.what()
152 );
153 }
154}

◆ repository_root()

fs::path anonymous_namespace{main.cpp}::repository_root ( )
nodiscard

Definition at line 67 of file main.cpp.

67 {
68#ifdef ARACHNE_SOURCE_DIR
69 return fs::path(ARACHNE_SOURCE_DIR);
70#else
71 return fs::current_path();
72#endif
73}

Referenced by command_path(), command_product_inbox(), command_product_rebuild_merge_hints(), conventional_legacy_inbox(), and load_configuration().

Here is the caller graph for this function:

◆ required_object()

const json & anonymous_namespace{main.cpp}::required_object ( const json & parent,
const std::string_view key )
nodiscard

Definition at line 313 of file main.cpp.

313 {
314 const auto value = parent.find(key);
315 if (value == parent.end() || !value->is_object()) {
316 throw cli_error(
317 "configuration " + std::string(key) + " must be an object"
318 );
319 }
320 return *value;
321}

◆ resolve_plan_artifact()

fs::path anonymous_namespace{main.cpp}::resolve_plan_artifact ( const configuration & config,
const fs::path & control_path,
const std::string_view storage_ref )
nodiscard

Definition at line 715 of file main.cpp.

718 {
720 throw cli_error(
721 "plan_artifact.storage_ref is not a safe relative path"
722 );
723 }
724 std::vector<fs::path> matches;
725 for (const fs::path& root :
726 std::array { control_path.parent_path(), config.artifact_store }) {
727 const fs::path candidate
728 = arachne::crypto::safe_artifact_path(root, storage_ref);
729 std::error_code error;
730 const auto state = fs::symlink_status(candidate, error);
731 if (error || !fs::is_regular_file(state) || fs::is_symlink(state)) {
732 continue;
733 }
734 if (!arachne::coordination::path_is_within(candidate, root)
735 || path_has_symlink(root, candidate)) {
736 throw cli_error(
737 "candidate plan artifact traverses a symbolic link"
738 );
739 }
740 const fs::path canonical = fs::canonical(candidate);
741 if (std::ranges::find(matches, canonical) == matches.end()) {
742 matches.push_back(canonical);
743 }
744 }
745 if (matches.empty()) {
746 throw cli_error(
747 "candidate plan artifact cannot be resolved beneath its control "
748 "directory or artifact store"
749 );
750 }
751 if (matches.size() != 1U) {
752 throw cli_error("candidate plan artifact reference is ambiguous");
753 }
754 return matches.front();
755}
bool path_has_symlink(const fs::path &root, const fs::path &candidate)
Definition main.cpp:698
std::filesystem::path safe_artifact_path(const std::filesystem::path &root, std::string_view artifact_ref)
Definition crypto.cpp:302
bool matches(const json *value, const std::regex &expression)

◆ resolve_snapshot_export()

resolved_snapshot_export anonymous_namespace{main.cpp}::resolve_snapshot_export ( const configuration & config,
const fs::path & control_path,
const arachnespace::contracts::contract_name expected_contract,
const std::string_view expected_kind )
nodiscard

Definition at line 762 of file main.cpp.

766 {
767 json control = read_json(
768 control_path, maximum_control_bytes, "graph snapshot control"
769 );
770 const auto validation
771 = arachnespace::contracts::validate(expected_contract, control);
772 if (!validation) {
773 throw cli_error(
774 validation_details(validation, "graph snapshot control")
775 );
776 }
777 const json* selected = nullptr;
778 for (const auto& value : control.at("exports")) {
779 if (value.is_object() && value.value("kind", "") == expected_kind) {
780 if (selected != nullptr) {
781 throw cli_error(
782 "snapshot control contains duplicate "
783 + std::string(expected_kind) + " exports"
784 );
785 }
786 selected = &value.at("artifact");
787 }
788 }
789 if (selected == nullptr) {
790 throw cli_error(
791 "snapshot control has no " + std::string(expected_kind) + " export"
792 );
793 }
794 const std::string storage_ref
795 = selected->at("storage_ref").get<std::string>();
797 throw cli_error("snapshot export storage_ref is unsafe");
798 }
799 std::vector<fs::path> matches;
800 for (const fs::path& root :
801 std::array { control_path.parent_path(), config.graph_store }) {
802 const fs::path candidate
803 = arachne::crypto::safe_artifact_path(root, storage_ref);
804 std::error_code error;
805 const auto state = fs::symlink_status(candidate, error);
806 if (error || !fs::is_regular_file(state) || fs::is_symlink(state)) {
807 continue;
808 }
809 if (!arachne::coordination::path_is_within(candidate, root)
810 || path_has_symlink(root, candidate)) {
811 throw cli_error("snapshot export traverses a symbolic link");
812 }
813 const fs::path canonical = fs::canonical(candidate);
814 if (std::ranges::find(matches, canonical) == matches.end()) {
815 matches.push_back(canonical);
816 }
817 }
818 if (matches.size() != 1U) {
819 throw cli_error(
820 matches.empty() ? "snapshot export cannot be resolved"
821 : "snapshot export reference is ambiguous"
822 );
823 }
824 const fs::path export_path = matches.front();
825 if (fs::file_size(export_path)
826 != selected->at("byte_length").get<std::uintmax_t>()
827 || arachne::crypto::sha256_file(export_path)
828 != selected->at("sha256").get<std::string>()) {
829 throw cli_error(
830 "snapshot export does not match its declared hash and byte length"
831 );
832 }
833 return { std::move(control), export_path };
834}

References arachne::crypto::sha256_file().

Here is the call graph for this function:

◆ resolved_path()

fs::path anonymous_namespace{main.cpp}::resolved_path ( const fs::path & path,
const fs::path & relative_root )
nodiscard

Definition at line 76 of file main.cpp.

76 {
77 std::error_code error;
78 fs::path candidate = path.is_absolute() ? path : relative_root / path;
79 fs::path result = fs::weakly_canonical(candidate, error);
80 if (!error) {
81 return result.lexically_normal();
82 }
83 error.clear();
84 result = fs::absolute(candidate, error);
85 if (error) {
86 throw cli_error("cannot resolve path: " + candidate.string());
87 }
88 return result.lexically_normal();
89}

Referenced by command_path(), and load_configuration().

Here is the caller graph for this function:

◆ size_value()

std::size_t anonymous_namespace{main.cpp}::size_value ( const json & object,
const std::string_view key,
const std::size_t fallback )
nodiscard

Definition at line 281 of file main.cpp.

283 {
284 const std::uint64_t value = unsigned_value(
285 object, key, static_cast<std::uint64_t>(fallback), true
286 );
287 if (value > std::numeric_limits<std::size_t>::max()) {
288 throw cli_error(
289 "configuration field " + std::string(key) + " is too large"
290 );
291 }
292 return static_cast<std::size_t>(value);
293}

◆ snapshot_json()

ordered_json anonymous_namespace{main.cpp}::snapshot_json ( const arachne::penelope::snapshot_result & snapshot)
nodiscard

Definition at line 579 of file main.cpp.

579 {
580 return {
581 { "domain", "research_candidate_graph" },
582 { "snapshot_id", snapshot.snapshot_id },
583 { "database_path", snapshot.database_path.generic_string() },
584 { "export_path", snapshot.export_path.generic_string() },
585 { "metadata_path", snapshot.metadata_path.generic_string() },
586 { "database_sha256", snapshot.database_sha256 },
587 { "export_sha256", snapshot.export_sha256 },
588 { "applied_inputs", snapshot.applied_inputs },
589 { "skipped_inputs", snapshot.skipped_inputs },
590 { "activated", snapshot.activated },
591 { "changed", snapshot.changed },
592 };
593}
std::filesystem::path metadata_path
Definition store.hpp:45
std::filesystem::path export_path
Definition store.hpp:44
std::filesystem::path database_path
Definition store.hpp:43

References arachne::penelope::snapshot_result::activated, arachne::penelope::snapshot_result::applied_inputs, arachne::penelope::snapshot_result::changed, arachne::penelope::snapshot_result::database_sha256, arachne::penelope::snapshot_result::export_sha256, arachne::penelope::snapshot_result::skipped_inputs, and arachne::penelope::snapshot_result::snapshot_id.

◆ unsigned_value()

std::uint64_t anonymous_namespace{main.cpp}::unsigned_value ( const json & object,
const std::string_view key,
const std::uint64_t default_value,
const bool positive )
nodiscard

Definition at line 252 of file main.cpp.

255 {
256 const auto value = object.find(key);
257 if (value == object.end()) {
258 return default_value;
259 }
260 if (!value->is_number_integer()) {
261 throw cli_error(
262 "configuration field " + std::string(key) + " must be an integer"
263 );
264 }
265 std::uint64_t result = 0;
266 try {
267 result = value->get<std::uint64_t>();
268 } catch (const json::exception&) {
269 throw cli_error(
270 "configuration field " + std::string(key) + " must be non-negative"
271 );
272 }
273 if (positive && result == 0U) {
274 throw cli_error(
275 "configuration field " + std::string(key) + " must be positive"
276 );
277 }
278 return result;
279}

◆ utc_now()

std::string anonymous_namespace{main.cpp}::utc_now ( )
nodiscard

Definition at line 156 of file main.cpp.

156 {
157 const std::time_t now = std::time(nullptr);
158 std::tm value {};
159 if (::gmtime_r(&now, &value) == nullptr) {
160 throw cli_error("cannot produce a UTC operation timestamp");
161 }
162 std::ostringstream output;
163 output << std::put_time(&value, "%Y-%m-%dT%H:%M:%SZ");
164 return output.str();
165}

Referenced by command_candidate_plan(), and command_viewer_build().

Here is the caller graph for this function:

◆ valid_logical_date()

bool anonymous_namespace{main.cpp}::valid_logical_date ( const std::string_view value)
nodiscard

Definition at line 1056 of file main.cpp.

1056 {
1057 if (value.size() != 10U || value[4] != '-' || value[7] != '-') {
1058 return false;
1059 }
1060 int year = 0;
1061 unsigned month = 0;
1062 unsigned day = 0;
1063 const auto year_result
1064 = std::from_chars(value.data(), value.data() + 4, year);
1065 const auto month_result
1066 = std::from_chars(value.data() + 5, value.data() + 7, month);
1067 const auto day_result
1068 = std::from_chars(value.data() + 8, value.data() + 10, day);
1069 if (year_result.ec != std::errc {} || month_result.ec != std::errc {}
1070 || day_result.ec != std::errc {}) {
1071 return false;
1072 }
1073 return std::chrono::year_month_day(
1074 std::chrono::year(year), std::chrono::month(month),
1075 std::chrono::day(day)
1076 )
1077 .ok();
1078}

◆ validation_details()

std::string anonymous_namespace{main.cpp}::validation_details ( const arachnespace::contracts::validation_result & result,
const std::string_view description )
nodiscard

Definition at line 171 of file main.cpp.

174 {
175 std::string message = std::string(description) + " validation failed";
176 for (const auto& diagnostic : result.diagnostics) {
177 message += "; "
178 + (diagnostic.instance_path.empty() ? std::string("/")
179 : diagnostic.instance_path)
180 + " [" + diagnostic.code + "] " + diagnostic.message;
181 }
182 return message;
183}
std::vector< diagnostic > diagnostics
Definition contracts.hpp:38

◆ verify_external_source_snapshot()

void anonymous_namespace{main.cpp}::verify_external_source_snapshot ( const configuration & config,
const json & external_graph )

Definition at line 836 of file main.cpp.

838 {
839 const auto& source = external_graph.at("source_snapshot");
840 const std::string storage_ref = source.at("storage_ref").get<std::string>();
842 throw cli_error("external source snapshot storage_ref is unsafe");
843 }
844 const fs::path artifact = arachne::crypto::safe_artifact_path(
845 config.artifact_store, storage_ref
846 );
847 std::error_code error;
848 const auto state = fs::symlink_status(artifact, error);
849 if (error || !fs::is_regular_file(state) || fs::is_symlink(state)
850 || path_has_symlink(config.artifact_store, artifact)
852 != source.at("sha256").get<std::string>()) {
853 throw cli_error(
854 "external source snapshot is unavailable or does not match its hash"
855 );
856 }
857}

References arachne::crypto::sha256_file().

Here is the call graph for this function:

◆ verify_product_coverage()

void anonymous_namespace{main.cpp}::verify_product_coverage ( const json & external_graph,
const json & product_tables )

Definition at line 859 of file main.cpp.

861 {
862 std::set<std::string, std::less<>> product_works;
863 for (const auto& work : product_tables.value("works", json::array())) {
864 if (work.is_object() && work.contains("entity_id")
865 && work.at("entity_id").is_string()) {
866 product_works.insert(work.at("entity_id").get<std::string>());
867 }
868 }
869 std::set<std::string, std::less<>> covered_external_ids;
870 for (const auto& identifier :
871 product_tables.value("external_ids", json::array())) {
872 if (identifier.is_object()
873 && identifier.value("scheme", "") == "wikidata"
874 && product_works.contains(identifier.value("entity_id", ""))
875 && identifier.contains("value")
876 && identifier.at("value").is_string()) {
877 covered_external_ids.insert(
878 identifier.at("value").get<std::string>()
879 );
880 }
881 }
882 for (const auto& work : external_graph.at("works")) {
883 const std::string id = work.at("id").get<std::string>();
884 const bool expected = covered_external_ids.contains(id);
885 if (!work.at("covered").is_boolean()
886 || work.at("covered").get<bool>() != expected) {
887 throw cli_error(
888 "external graph coverage disagrees with the verified product "
889 "snapshot for work "
890 + id
891 );
892 }
893 }
894}
std::string identifier(const nlohmann::json &value, std::string_view integer_namespace={})
Definition viewer.cpp:89

◆ wikidata_bulk_fetch_request()

ordered_json anonymous_namespace{main.cpp}::wikidata_bulk_fetch_request ( const json & plan,
const json & planned,
std::string request_id,
std::string locator )
nodiscard

Definition at line 1698 of file main.cpp.

1701 {
1702 constexpr std::string_view dump_base
1703 = "https://dumps.wikimedia.org/wikidatawiki/entities/";
1704 if (!locator.starts_with(dump_base)) {
1705 throw cli_error(
1706 "Wikidata bulk fetch locator must use the official dump endpoint"
1707 );
1708 }
1709 const std::size_t locator_end = locator.find_first_of("?#");
1710 if (locator_end != std::string::npos) {
1711 throw cli_error(
1712 "Wikidata bulk fetch locator cannot contain a query or fragment"
1713 );
1714 }
1715 const std::string_view locator_path(locator);
1716 std::string compression_suffix;
1717 for (const std::string_view supported : {
1718 std::string_view { ".json.bz2" },
1719 std::string_view { ".json.gz" },
1720 std::string_view { ".json" },
1721 }) {
1722 if (locator_path.ends_with(supported)) {
1723 compression_suffix = supported;
1724 break;
1725 }
1726 }
1727 if (compression_suffix.empty()) {
1728 throw cli_error(
1729 "Wikidata bulk fetch locator has an unsupported dump encoding"
1730 );
1731 }
1732 const std::string output_ref = "bulk/"
1733 + plan.at("plan_id").get<std::string>() + "/" + request_id
1734 + compression_suffix;
1735 ordered_json document {
1736 { "contract", "fetch_request_v1" },
1737 { "format_version", 1 },
1738 { "request_id", request_id },
1739 { "door_id", "wikidata" },
1740 { "endpoint_id", "official-dumps" },
1741 { "operation", "bulk_snapshot" },
1742 { "freshness_policy", "fresh_required" },
1743 { "plan_id", plan.at("plan_id") },
1744 { "locator", std::move(locator) },
1745 { "method", "GET" },
1746 { "headers",
1747 { { "Accept", "application/octet-stream" },
1748 { "User-Agent",
1749 "Arachne/2.0 (+https://github.com/ninjaro/arachne)" } } },
1750 { "pagination", { { "mode", "none" } } },
1751 { "retry",
1752 { { "maximum_attempts", 5 },
1753 { "initial_delay_ms", 1000 },
1754 { "maximum_delay_ms", 60000 },
1755 { "total_delay_budget_ms", 300000 },
1756 { "respect_retry_after", true } } },
1757 { "expected",
1758 { { "maximum_bytes", 1099511627776ULL },
1759 { "timeout_ms", 86400000 },
1760 { "connect_timeout_ms", 30000 },
1761 { "read_timeout_ms", 900000 },
1762 { "write_timeout_ms", 30000 } } },
1763 { "redirect_policy",
1764 { { "follow", false },
1765 { "maximum_redirects", 0 },
1766 { "allow_https_to_http", false },
1767 { "allowed_hosts", { "dumps.wikimedia.org" } } } },
1768 { "output_ref", output_ref },
1769 };
1770 static_cast<void>(planned);
1771 const auto validation = arachnespace::contracts::validate(
1773 );
1774 if (!validation) {
1775 throw cli_error(
1776 validation_details(validation, "translated bulk request")
1777 );
1778 }
1779 return document;
1780}

References arachnespace::contracts::fetch_request.

◆ wikidata_entity_id()

bool anonymous_namespace{main.cpp}::wikidata_entity_id ( const std::string_view value)
nodiscard

Definition at line 1566 of file main.cpp.

1566 {
1567 return value.size() >= 2U
1568 && (value.front() == 'Q' || value.front() == 'P' || value.front() == 'L'
1569 || value.front() == 'M')
1570 && std::ranges::all_of(value.substr(1U), [](const char character) {
1571 return character >= '0' && character <= '9';
1572 });
1573}

◆ wikidata_point_fetch_request()

ordered_json anonymous_namespace{main.cpp}::wikidata_point_fetch_request ( const configuration & config,
const json & plan,
const json & planned,
const std::span< const std::string > entities,
const std::string & request_id )
nodiscard

Definition at line 1588 of file main.cpp.

1591 {
1592 static const std::map<std::string, std::string, std::less<>> field_props {
1593 { "labels", "labels" }, { "descriptions", "descriptions" },
1594 { "aliases", "aliases" }, { "sitelinks", "sitelinks" },
1595 { "claims", "claims" }, { "gender", "claims" },
1596 { "country", "claims" }, { "field", "claims" },
1597 { "occupation", "claims" }, { "movement", "claims" },
1598 { "genre", "claims" }, { "language", "claims" },
1599 { "activity_dates", "claims" }, { "dates", "claims" },
1600 };
1601 if (!planned.contains("fields") || !planned.at("fields").is_array()
1602 || planned.at("fields").empty()) {
1603 throw cli_error(
1604 "Wikidata entity fetches require a non-empty fields selector"
1605 );
1606 }
1607 std::set<std::string, std::less<>> props;
1608 for (const auto& field : planned.at("fields")) {
1609 if (!field.is_string()) {
1610 throw cli_error("Wikidata fetch field must be a string");
1611 }
1612 const auto found
1613 = field_props.find(field.get_ref<const std::string&>());
1614 if (found == field_props.end()) {
1615 throw cli_error(
1616 "unsupported Wikidata fetch field: " + field.get<std::string>()
1617 );
1618 }
1619 props.insert(found->second);
1620 }
1621 // Claim-oriented profile fields still require human-readable context.
1622 if (props.contains("claims")) {
1623 props.insert("labels");
1624 props.insert("descriptions");
1625 }
1626 const std::vector<std::string> ordered_props(props.begin(), props.end());
1627 const std::string body = "action=wbgetentities&format=json&ids="
1628 + form_encode(join_strings(entities, "|"))
1629 + "&props=" + form_encode(join_strings(ordered_props, "|"))
1630 + "&languages=en&languagefallback=1";
1631 const fs::path body_path = config.artifact_store / "fetch-bodies"
1632 / plan.at("plan_id").get<std::string>() / (request_id + ".form");
1633 const fs::path body_relative
1634 = body_path.lexically_relative(config.artifact_store);
1636 body_relative.generic_string()
1637 )) {
1638 throw cli_error(
1639 "generated fetch body has an unsafe artifact reference"
1640 );
1641 }
1642 write_immutable_exact(body_path, body, "fetch request body artifact");
1643
1644 ordered_json document {
1645 { "contract", "fetch_request_v1" },
1646 { "format_version", 1 },
1647 { "request_id", request_id },
1648 { "door_id", "wikidata" },
1649 { "endpoint_id", "entity-api" },
1650 { "operation", "point_lookup" },
1651 { "freshness_policy", "fresh_required" },
1652 { "plan_id", plan.at("plan_id") },
1653 { "locator", planned.at("locator") },
1654 { "method", "POST" },
1655 { "headers",
1656 { { "Accept", "application/json" },
1657 { "Content-Type", "application/x-www-form-urlencoded" },
1658 { "User-Agent",
1659 "Arachne/2.0 (+https://github.com/ninjaro/arachne)" } } },
1660 { "pagination", { { "mode", "none" } } },
1661 { "retry",
1662 { { "maximum_attempts", 3 },
1663 { "initial_delay_ms", 250 },
1664 { "maximum_delay_ms", 10000 },
1665 { "total_delay_budget_ms", 30000 },
1666 { "respect_retry_after", true } } },
1667 { "expected",
1668 { { "maximum_bytes", 16777216 },
1669 { "timeout_ms", 60000 },
1670 { "connect_timeout_ms", 10000 },
1671 { "read_timeout_ms", 30000 },
1672 { "write_timeout_ms", 30000 } } },
1673 { "redirect_policy",
1674 { { "follow", false },
1675 { "maximum_redirects", 0 },
1676 { "allow_https_to_http", false },
1677 { "allowed_hosts", { "www.wikidata.org" } } } },
1678 { "output_ref",
1679 "acquired/" + plan.at("plan_id").get<std::string>() + "/" + request_id
1680 + ".json" },
1681 { "body_artifact",
1682 { { "storage_ref", body_relative.generic_string() },
1683 { "sha256", arachne::crypto::sha256(body) },
1684 { "byte_length", body.size() },
1685 { "media_type", "application/x-www-form-urlencoded" } } },
1686 };
1687 const auto validation = arachnespace::contracts::validate(
1689 );
1690 if (!validation) {
1691 throw cli_error(
1692 validation_details(validation, "translated fetch request")
1693 );
1694 }
1695 return document;
1696}
std::string join_strings(const std::span< const std::string > values, const std::string_view separator)
Definition main.cpp:1575
std::string form_encode(const std::string_view value)
Definition main.cpp:1548
const json * field(const json &object, const std::string_view key, const std::string_view path, validation_result &result)
Definition contracts.cpp:57

References arachnespace::contracts::fetch_request, and arachne::crypto::sha256().

Here is the call graph for this function:

◆ write_graph_run_manifest()

written_run_manifest anonymous_namespace{main.cpp}::write_graph_run_manifest ( const configuration & config,
const std::string_view domain_directory,
const std::string_view graph_domain,
const std::string_view run_id,
ordered_json configuration_hashes,
ordered_json inputs,
const arachne::penelope::snapshot_result & snapshot )
nodiscard

Definition at line 601 of file main.cpp.

606 {
607 const json metadata = read_json(
608 snapshot.metadata_path, maximum_control_bytes, "snapshot metadata"
609 );
610 ordered_json outputs = ordered_json::array();
611 outputs.push_back(
612 { { "kind", "graph-database" },
613 { "artifact", metadata.at("database") } }
614 );
615 for (const auto& exported : metadata.at("exports")) {
616 outputs.push_back(
617 { { "kind", exported.at("kind") },
618 { "artifact", exported.at("artifact") } }
619 );
620 }
621 outputs.push_back(
622 { { "kind", "structural-validation-report" },
623 { "artifact", metadata.at("structural_validation").at("report") } }
624 );
625 const fs::path metadata_ref
626 = snapshot.metadata_path.lexically_relative(config.graph_store);
627 if (metadata_ref.empty()
629 metadata_ref.generic_string()
630 )) {
631 throw cli_error("snapshot metadata has no safe graph-store reference");
632 }
633 outputs.push_back(
634 { { "kind", "snapshot-control" },
635 { "artifact",
636 { { "storage_ref", metadata_ref.generic_string() },
637 { "sha256",
639 { "byte_length", fs::file_size(snapshot.metadata_path) },
640 { "media_type", "application/json" } } } }
641 );
642
643 ordered_json manifest {
644 { "manifest_type", "arachne_run_manifest_v1" },
645 { "format_version", 1 },
646 { "run_id", std::string(run_id) },
647 { "graph_domain", std::string(graph_domain) },
648 { "generated_at", metadata.at("activated_at") },
649 { "actor_versions",
650 { { "arachne", "2.0.0" },
651 { "pheidippides", "pheidippides-transport-2.0.0" },
652 { "ariadne", "ariadne-engine-2.0.0" },
653 { "penelope", "penelope-store-2.0.0" } } },
654 { "contract_versions",
655 { { "controls",
656 { "arachne_batch_v2", "batch_envelope_v1", "fetch_plan_v1",
657 "fetch_request_v1", "acquired_artifact_v1",
658 "research_candidate_graph_plan_v1", "product_graph_snapshot_v1",
659 "research_candidate_graph_snapshot_v1", "viewer_projection_v1",
660 "site_bundle_v1" } },
661 { "artifacts",
662 { "external_candidate_source_graph_v1",
663 "research_candidate_graph_materialization_v1",
664 "viewer_projection_data_v1" } } } },
665 { "configuration_hashes", std::move(configuration_hashes) },
666 { "inputs", std::move(inputs) },
667 { "outputs", std::move(outputs) },
668 { "structural_validation", metadata.at("structural_validation") },
669 };
670 const fs::path path = config.graph_store / domain_directory / "runs"
671 / (std::string(run_id) + ".json");
672 const fs::path relative = path.lexically_relative(config.graph_store);
674 relative.generic_string()
675 )) {
676 throw cli_error("run manifest has no safe graph-store reference");
677 }
678 const std::string bytes
680 if (fs::exists(path)) {
681 if (read_bytes(path, maximum_control_bytes, "existing run manifest")
682 != bytes) {
683 throw cli_error(
684 "run manifest identity is already bound to different content"
685 );
686 }
687 } else {
688 atomic_write(path, bytes, false);
689 }
690 return { std::move(manifest), path, relative.generic_string() };
691}

References atomic_write(), maximum_control_bytes, and arachne::crypto::sha256_file().

Here is the call graph for this function:

◆ write_immutable_exact()

void anonymous_namespace{main.cpp}::write_immutable_exact ( const fs::path & destination,
const std::string_view bytes,
const std::string_view description )

Definition at line 539 of file main.cpp.

542 {
543 if (fs::exists(destination)) {
544 if (read_bytes(
545 destination, maximum_control_bytes,
546 std::string("existing ") + std::string(description)
547 )
548 != bytes) {
549 throw cli_error(
550 std::string(description)
551 + " identity is already bound to different content"
552 );
553 }
554 return;
555 }
556 atomic_write(destination, bytes, false);
557}

References maximum_control_bytes, and read_bytes().

Here is the call graph for this function:

Variable Documentation

◆ maximum_config_bytes

std::uintmax_t anonymous_namespace{main.cpp}::maximum_config_bytes = 4U * 1024U * 1024U
constexpr

Definition at line 49 of file main.cpp.

◆ maximum_control_bytes

std::uintmax_t anonymous_namespace{main.cpp}::maximum_control_bytes = 64U * 1024U * 1024U
constexpr

Definition at line 50 of file main.cpp.

Referenced by write_graph_run_manifest(), and write_immutable_exact().

◆ maximum_export_bytes

std::uintmax_t anonymous_namespace{main.cpp}::maximum_export_bytes = 1024ULL * 1024ULL * 1024ULL
constexpr

Definition at line 51 of file main.cpp.

◆ temporary_sequence

std::atomic< std::uint64_t > anonymous_namespace{main.cpp}::temporary_sequence { 0 }

Definition at line 53 of file main.cpp.

53{ 0 };

Referenced by atomic_write().