785 {
788 auto pool =
rank_pool(graph, configuration);
789 const auto pool_count = pool.size();
791
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);
821 plan["candidates"].push_back(
822 { { "candidate_id", candidate_id },
823 { "external_id", agent.id },
824 { "label", agent.label },
825 { "kind", "candidate" },
827 { "coverage",
828 static_cast<double>(
candidate.coverage_basis_points) / 100.0 },
829 {
"group_id",
"group-" + std::to_string(
candidate.group) },
830 { "selection_reasons",
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_"
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) {
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_"
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)
std::string canonical_json(const nlohmann::json &document)