Arachne 1.0
Arachne - the perpetual stitcher of Wikidata entities.
Loading...
Searching...
No Matches
arachne::ariadne::candidate_planner Class Reference

#include <include/ariadne/candidates.hpp>

Static Public Member Functions

static nlohmann::ordered_json configuration_values (const candidate_configuration &configuration={})
static nlohmann::ordered_json build (const nlohmann::json &external_graph, const candidate_configuration &configuration={})
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 enrichment_fetch_plan (const nlohmann::json &candidate_pool, const nlohmann::json &available_profiles, std::string source_name, std::string locator, std::string created_at)

Detailed Description

Definition at line 20 of file candidates.hpp.

Member Function Documentation

◆ build()

nlohmann::ordered_json arachne::ariadne::candidate_planner::build ( const nlohmann::json & external_graph,
const candidate_configuration & configuration = {} )
staticnodiscard

Definition at line 782 of file candidates.cpp.

785 {
786 validate_configuration(configuration);
787 const auto graph = parse_graph(external_graph);
788 auto pool = rank_pool(graph, configuration);
789 const auto pool_count = pool.size();
790 group_and_select(pool, graph, configuration);
791
792 const auto values = configuration_values(configuration);
793 const auto configuration_hash
795
796 nlohmann::ordered_json plan {
797 { "artifact_type", "research_candidate_graph_materialization_v1" },
798 { "format_version", 1 },
799 { "source_snapshot",
800 { { "snapshot_id", graph.source_snapshot_id },
801 { "storage_ref", graph.source_storage_ref },
802 { "sha256", graph.source_sha256 } } },
803 { "algorithm",
804 { { "name", "wikidata_art_multi_pass" },
805 { "version", "1.0.0" },
806 { "configuration_sha256", configuration_hash } } },
807 { "groups", nlohmann::ordered_json::array() },
808 { "candidates", nlohmann::ordered_json::array() },
809 { "works", nlohmann::ordered_json::array() },
810 { "relations", nlohmann::ordered_json::array() },
811 };
812
813 std::vector<std::size_t> group_candidates(configuration.group_count, 0);
814 std::vector<std::size_t> group_works(configuration.group_count, 0);
815 std::set<std::size_t> emitted_works;
816 for (const auto& candidate : pool) {
817 const auto& agent = graph.agents[candidate.agent_index];
818 const auto candidate_id = "candidate-" + agent.id;
819 ++group_candidates.at(candidate.group - 1);
820 group_works.at(candidate.group - 1) += candidate.claimed_works.size();
821 plan["candidates"].push_back(
822 { { "candidate_id", candidate_id },
823 { "external_id", agent.id },
824 { "label", agent.label },
825 { "kind", "candidate" },
826 { "rank", candidate.rank },
827 { "coverage",
828 static_cast<double>(candidate.coverage_basis_points) / 100.0 },
829 { "group_id", "group-" + std::to_string(candidate.group) },
830 { "selection_reasons",
831 { selection_explanation(candidate),
832 "Candidate state was recomputed from the declared source and "
833 "product coverage; no prior grey-node state was reused." } },
834 { "source_snapshot_id", graph.source_snapshot_id },
835 { "attributes",
836 { { "coverage_basis_points", candidate.coverage_basis_points },
837 { "parsed_children", candidate.parsed_children },
838 { "gray_children", candidate.gray_children },
839 { "total_active_children", candidate.total_children },
840 { "grouping_score",
841 std::round(candidate.grouping_score * 100000000.0)
842 / 100000000.0 },
843 { "profile", agent.profile },
844 { "ranked_pool_size", pool_count },
845 { "noncanonical", true } } } }
846 );
847 for (const auto work_index : candidate.claimed_works) {
848 const auto& work = graph.works[work_index];
849 const auto work_id = "candidate-work-" + work.id;
850 if (emitted_works.emplace(work_index).second) {
851 plan["works"].push_back(
852 { { "work_id", work_id },
853 { "candidate_id", candidate_id },
854 { "external_id", work.id },
855 { "label", work.label },
856 { "source_snapshot_id", graph.source_snapshot_id },
857 { "attributes",
858 { { "noncanonical", true },
859 { "soft_guidance", true } } } }
860 );
861 }
862 plan["relations"].push_back(
863 { { "relation_id",
864 "suggestion_"
865 + crypto::sha256(agent.id + "\n" + work.id)
866 .substr(0, 24) },
867 { "source_id", candidate_id },
868 { "target_id", work_id },
869 { "relation_type", "research_suggestion" },
870 { "weight", 1.0 },
871 { "provenance",
872 { { "origin", "algorithmic_external" },
873 { "source_snapshot_id", graph.source_snapshot_id },
874 { "algorithm_version", "1.0.0" },
875 { "explanation",
876 "The work was an unclaimed graph neighbor; this soft "
877 "suggestion does not reserve or verify it." } } },
878 { "attributes", { { "soft_guidance", true } } } }
879 );
880 }
881 }
882 for (std::size_t group = 0; group < configuration.group_count; ++group) {
883 plan["groups"].push_back(
884 { { "group_id", "group-" + std::to_string(group + 1) },
885 { "label",
886 "Research suggestion group " + std::to_string(group + 1) },
887 { "order", group },
888 { "candidate_count", group_candidates[group] },
889 { "rationale",
890 "Balanced deterministic grouping by available profile and "
891 "temporal features." },
892 { "attributes",
893 { { "work_count", group_works[group] },
894 { "soft_guidance", true } } } }
895 );
896 }
897 auto sorted_works
898 = plan["works"].get<std::vector<nlohmann::ordered_json>>();
899 std::ranges::sort(sorted_works, [](const auto& left, const auto& right) {
900 return id_less(
901 left.at("work_id").template get_ref<const std::string&>(),
902 right.at("work_id").template get_ref<const std::string&>()
903 );
904 });
905 plan["works"] = std::move(sorted_works);
906 const std::string canonical_without_id = plan.dump();
907 plan["plan_id"] = "candidate_plan_"
908 + crypto::sha256(canonical_without_id).substr(0, 32);
909 return plan;
910}
static nlohmann::ordered_json configuration_values(const candidate_configuration &configuration={})
std::vector< ranked_candidate > rank_pool(const graph_input &graph, const candidate_configuration &configuration)
std::string selection_explanation(const ranked_candidate &candidate)
bool id_less(std::string_view left, std::string_view right)
void validate_configuration(const candidate_configuration &configuration)
graph_input parse_graph(const nlohmann::json &document)
void group_and_select(std::vector< ranked_candidate > &pool, const graph_input &graph, const candidate_configuration &configuration)
std::string sha256(std::span< const std::byte > bytes)
Definition crypto.cpp:209
std::string canonical_json(const nlohmann::json &document)

References arachne::ariadne::candidate_configuration::group_count, arachne::crypto::sha256(), and arachne::ariadne::anonymous_namespace{candidates.cpp}::validate_configuration().

Here is the call graph for this function:

◆ configuration_values()

nlohmann::ordered_json arachne::ariadne::candidate_planner::configuration_values ( const candidate_configuration & configuration = {})
staticnodiscard

Canonical values whose hash identifies one candidate run policy.

Definition at line 767 of file candidates.cpp.

769 {
770 validate_configuration(configuration);
771 return {
772 { "candidate_pool_size", configuration.pool_size },
773 { "final_target", configuration.target_size },
774 { "group_count", configuration.group_count },
775 { "gray_bonus_basis_points", configuration.gray_bonus_basis_points },
776 { "quality_weight", configuration.quality_weight },
777 { "grey_node_policy", "recompute" },
778 { "tie_breaker", "stable-id" },
779 };
780}

References arachne::ariadne::candidate_configuration::gray_bonus_basis_points, arachne::ariadne::candidate_configuration::group_count, arachne::ariadne::candidate_configuration::pool_size, arachne::ariadne::candidate_configuration::quality_weight, arachne::ariadne::candidate_configuration::target_size, and arachne::ariadne::anonymous_namespace{candidates.cpp}::validate_configuration().

Here is the call graph for this function:

◆ enrichment_fetch_plan()

nlohmann::ordered_json arachne::ariadne::candidate_planner::enrichment_fetch_plan ( const nlohmann::json & candidate_pool,
const nlohmann::json & available_profiles,
std::string source_name,
std::string locator,
std::string created_at )
staticnodiscard

Definition at line 986 of file candidates.cpp.

990 {
991 if (!candidate_pool.is_array() || !available_profiles.is_object()
992 || source_name.empty() || locator.empty() || created_at.empty()) {
993 throw std::invalid_argument("invalid enrichment planning input");
994 }
995 std::vector<std::string> missing;
996 for (const auto& candidate : candidate_pool) {
997 const auto id = candidate.contains("external_id")
998 ? required_string(candidate, "external_id", "candidate")
999 : required_string(candidate, "id", "candidate");
1000 if (!available_profiles.contains(id)) {
1001 missing.push_back(id);
1002 }
1003 }
1004 std::ranges::sort(missing, id_less);
1005 nlohmann::ordered_json result {
1006 { "contract", "fetch_plan_v1" },
1007 { "format_version", 1 },
1008 { "plan_id", "" },
1009 { "source", std::move(source_name) },
1010 { "requests",
1011 nlohmann::ordered_json::array(
1012 { { { "request_id", "candidate-profile-enrichment" },
1013 { "locator", std::move(locator) },
1014 { "purpose", "candidate profile enrichment" },
1015 { "entities", missing },
1016 { "fields",
1017 { "gender", "country", "field", "occupation", "movement",
1018 "genre", "language", "activity_dates" } },
1019 { "follow_up", true } } }
1020 ) },
1021 { "created_at", std::move(created_at) },
1022 };
1023 result["plan_id"]
1024 = "fetch_plan_" + crypto::sha256(result.dump()).substr(0, 32);
1025 return result;
1026}
std::string required_string(const nlohmann::json &value, std::string_view field, std::string_view context)

References arachne::crypto::sha256().

Here is the call graph for this function:

◆ write_plan()

nlohmann::ordered_json arachne::ariadne::candidate_planner::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 )
staticnodiscard

Publish a resolved materialization artifact and return its validated research_candidate_graph_plan_v1 control contract.

Definition at line 912 of file candidates.cpp.

917 {
918 if (!materialization.is_object()
919 || materialization.value("artifact_type", "")
920 != "research_candidate_graph_materialization_v1"
921 || materialization.value("format_version", 0) != 1
922 || storage_ref.empty()) {
923 throw std::invalid_argument(
924 "plan publication requires a resolved candidate materialization"
925 );
926 }
927 for (const auto* field : { "groups", "candidates", "works", "relations" }) {
928 if (!materialization.contains(field)
929 || !materialization.at(field).is_array()) {
930 throw std::invalid_argument(
931 std::string("candidate materialization requires array ") + field
932 );
933 }
934 }
935 const auto values = configuration_values(configuration);
936 const auto configuration_hash
938 if (!materialization.contains("algorithm")
939 || !materialization.at("algorithm").is_object()
940 || materialization.at("algorithm").value("configuration_sha256", "")
941 != configuration_hash) {
942 throw std::invalid_argument(
943 "candidate configuration does not match materialization"
944 );
945 }
946 const std::string bytes = materialization.dump(2) + "\n";
947 nlohmann::ordered_json contract {
948 { "contract", "research_candidate_graph_plan_v1" },
949 { "format_version", 1 },
950 { "plan_id", materialization.at("plan_id") },
951 { "source_snapshot", materialization.at("source_snapshot") },
952 { "product_snapshot",
953 { { "snapshot_id", std::move(product_snapshot_id) },
954 { "sha256", std::move(product_snapshot_sha256) } } },
955 { "algorithm_version",
956 materialization.at("algorithm").at("name").get<std::string>() + "-"
957 + materialization.at("algorithm")
958 .at("version")
959 .get<std::string>() },
960 { "configuration",
961 { { "sha256", configuration_hash }, { "values", values } } },
962 { "plan_artifact",
963 { { "storage_ref", std::move(storage_ref) },
964 { "sha256", crypto::sha256(bytes) },
965 { "byte_length", bytes.size() },
966 { "media_type", "application/json" } } },
967 { "summary",
968 { { "candidate_count", materialization.at("candidates").size() },
969 { "edge_count", materialization.at("relations").size() },
970 { "group_count", materialization.at("groups").size() } } },
971 { "created_at", std::move(created_at) },
972 };
973 const auto validation = arachnespace::contracts::validate(
975 contract
976 );
977 if (!validation.valid()) {
978 throw std::invalid_argument(
979 "generated research_candidate_graph_plan_v1 contract is invalid"
980 );
981 }
982 publish_immutable_file(destination, bytes);
983 return contract;
984}
void publish_immutable_file(const std::filesystem::path &destination, std::string_view bytes)
validation_result validate(const nlohmann::json &document)

References arachne::ariadne::anonymous_namespace{candidates.cpp}::publish_immutable_file(), arachnespace::contracts::research_candidate_graph_plan, and arachne::crypto::sha256().

Here is the call graph for this function:

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