22namespace policy = core::domain::policies::matching;
26constexpr double kSyntheticConfidence = 0.35;
27constexpr double kSuggestionYellowThreshold = 0.4;
28constexpr double kSuggestionGreenThreshold = 0.9;
30std::string normalizeAllocatableMode(std::string value) {
31 auto normalized = policy::trim(std::move(value));
33 normalized.begin(), normalized.end(), normalized.begin(),
34 [](
unsigned char c) { return static_cast<char>(std::tolower(c)); });
35 if (normalized ==
"non allocatable")
36 normalized =
"non-allocatable";
40bool containsId(
const std::vector<std::string> &ids,
const std::string &
id) {
41 return std::find(ids.begin(), ids.end(),
id) != ids.end();
44bool appendUnique(std::vector<std::string> &values,
const std::string &value) {
45 if (value.empty() || containsId(values, value))
47 values.push_back(value);
51template <
typename TEntity>
52std::vector<core::domain::Alias> aliasUsages(
const TEntity &entity) {
53 std::vector<core::domain::Alias> out;
54 out.reserve(entity.aliases().size());
55 for (
const auto &alias : entity.aliases()) {
56 if (alias.value().empty())
63const DraftSuggestionCandidate *
64topSuggestion(
const DraftSuggestionBucket &bucket) {
65 return bucket.candidates.empty() ? nullptr : &bucket.candidates.front();
68int confidencePercent(
const DraftSuggestionCandidate *suggestion) {
69 return suggestion ?
static_cast<int>(std::round(
70 std::clamp(suggestion->confidence, 0.0, 1.0) * 100.0))
74std::string actorDisplay(
const core::domain::Actor &actor) {
78std::string propertyDisplay(
const core::domain::Property &property) {
79 return property.name();
82DraftChoiceRow
actorRow(
const std::shared_ptr<core::domain::Actor> &actor) {
87 row.name = actor->name();
88 row.display = actorDisplay(*actor);
89 for (
const auto &alias : actor->aliases()) {
90 row.aliases.push_back(alias.value());
96propertyRow(
const std::shared_ptr<core::domain::Property> &property) {
100 row.id =
property->id();
101 row.name =
property->name();
102 row.display = propertyDisplay(*property);
103 for (
const auto &alias : property->aliases()) {
104 row.aliases.push_back(alias.value());
110contractRow(
const std::shared_ptr<core::domain::Contract> &contract) {
114 row.id = contract->id();
115 row.name = contract->name();
116 row.display = contract->name();
117 row.type = contract->type();
118 row.allocatableMode = contract->allocatableMode();
119 for (
const auto &alias : contract->aliases()) {
120 row.aliases.push_back(alias.value());
122 row.actorIds = contract->actorIds();
123 row.propertyIds = contract->propertyIds();
127bool hasAnyPropertyOverlap(
const std::vector<std::string> &lhs,
128 const std::vector<std::string> &rhs) {
129 for (
const auto &
id : lhs) {
130 if (containsId(rhs,
id))
136std::vector<DraftChoiceRow>
137actorRows(
const core::domain::catalog::WorkspaceCatalog &state) {
138 std::vector<DraftChoiceRow> rows;
139 rows.reserve(state.
actors().size());
140 for (
const auto &actor : state.
actors()) {
143 rows.push_back(std::move(row));
148std::vector<DraftChoiceRow>
149propertyRows(
const core::domain::catalog::WorkspaceCatalog &state) {
150 std::vector<DraftChoiceRow> rows;
152 for (
const auto &property : state.
properties()) {
155 rows.push_back(std::move(row));
160std::vector<DraftChoiceRow>
161contractRows(
const core::domain::catalog::WorkspaceCatalog &state) {
162 std::vector<DraftChoiceRow> rows;
164 for (
const auto &contract : state.
contracts()) {
167 rows.push_back(std::move(row));
172int rowIndexById(
const std::vector<DraftChoiceRow> &rows,
173 const std::string &
id) {
176 for (std::size_t i = 0; i < rows.size(); ++i) {
177 if (rows[i].
id ==
id)
178 return static_cast<int>(i);
183const DraftChoiceRow *rowByIndex(
const std::vector<DraftChoiceRow> &rows,
185 return (index >= 0 &&
static_cast<std::size_t
>(index) < rows.size())
186 ? &rows[
static_cast<std::size_t
>(index)]
190std::string firstExistingText(
const DraftChoiceRow &row) {
191 if (!row.display.empty())
193 if (!row.name.empty())
195 if (!row.type.empty())
200bool rowMatchesText(
const DraftChoiceRow &row,
const std::string &text) {
201 const auto key = policy::normalizeText(text);
205 if (policy::normalizeText(row.name) == key)
207 if (policy::normalizeText(row.display) == key)
209 if (policy::normalizeText(row.type) == key)
211 for (
const auto &alias : row.aliases) {
212 if (policy::normalizeText(alias) == key)
218const DraftChoiceRow *findRowByText(
const std::vector<DraftChoiceRow> &rows,
219 const std::string &text) {
220 for (
const auto &row : rows) {
221 if (rowMatchesText(row, text))
227std::string actorDisplayText(
const DraftLinkSelection &selection,
228 const std::vector<DraftChoiceRow> &rows,
229 const DraftSuggestionCandidate *top) {
230 if (!selection.actorId.empty()) {
231 const auto index = rowIndexById(rows, selection.actorId);
232 const auto *row = rowByIndex(rows, index);
233 return row ? firstExistingText(*row) : selection.actorId;
235 if (top && !top->label.empty())
237 const auto actorText = policy::trim(selection.actorText);
238 if (!actorText.empty())
243std::string contractDisplayText(
const DraftLinkSelection &selection,
244 const std::vector<DraftChoiceRow> &rows,
245 const DraftSuggestionCandidate *top) {
246 if (!selection.contractId.empty()) {
247 const auto index = rowIndexById(rows, selection.contractId);
248 const auto *row = rowByIndex(rows, index);
250 if (!row->name.empty())
252 return firstExistingText(*row);
254 return selection.contractId;
260std::string actorSeedText(
const DraftLinkSelection &selection,
261 const std::string &displayText,
262 const DraftSuggestionCandidate *top) {
263 if (!selection.actorId.empty())
265 const auto actorText = policy::trim(selection.actorText);
266 if (selection.actorSelected && actorText.empty())
268 if (!actorText.empty())
270 return top ? top->label : std::string{};
273std::string contractSeedText(
const DraftLinkSelection &selection,
274 const std::string &displayText,
275 const DraftSuggestionCandidate *top) {
276 if (!selection.contractId.empty())
278 const auto type = policy::trim(selection.type);
279 if (selection.contractSelected && type.empty())
286std::string propertySuggestionSummary(
const DraftSuggestionBucket &bucket) {
287 const auto *top = topSuggestion(bucket);
289 return "0% Confidence - No suggestion";
290 const auto pct = confidencePercent(top);
291 if (top->confidence < kSuggestionYellowThreshold) {
292 return std::to_string(pct) +
"% Confidence - No suggestion";
295 std::vector<std::string> labels;
296 const auto maxCount = std::min<std::size_t>(2, bucket.candidates.size());
297 labels.reserve(maxCount);
298 for (std::size_t i = 0; i < maxCount; ++i) {
299 if (!bucket.candidates[i].label.empty())
300 labels.push_back(bucket.candidates[i].label);
304 return std::to_string(pct) +
"% Confidence - No suggestion";
305 return std::to_string(pct) +
"% Confidence - (" +
306 policy::joinNonEmptyLines(labels,
", ") +
")";
309std::string actorSuggestionSummary(
const DraftSuggestionBucket &bucket) {
310 const auto *top = topSuggestion(bucket);
312 return "0% Confidence - No suggestion";
313 const auto pct = confidencePercent(top);
314 if (top->confidence < kSuggestionYellowThreshold) {
315 return std::to_string(pct) +
"% Confidence - No suggestion";
317 if (top->label.empty())
318 return std::to_string(pct) +
"% Confidence - No suggestion";
319 return std::to_string(pct) +
"% Confidence - " + top->label;
323contractSuggestionSummary(
const DraftDerivedState &derived,
324 const std::vector<DraftChoiceRow> &contractChoices,
325 const std::string &selectedContractId) {
326 const auto confidence =
327 std::clamp(derived.contractSuggestionConfidence, 0.0, 1.0);
328 const auto confidencePercent =
329 static_cast<int>(std::round(confidence * 100.0));
330 if (confidence < kSuggestionYellowThreshold) {
331 return std::to_string(confidencePercent) +
"% Confidence - No suggestion";
334 const DraftChoiceRow *row =
nullptr;
335 if (!selectedContractId.empty()) {
336 const auto idx = rowIndexById(contractChoices, selectedContractId);
337 row = rowByIndex(contractChoices, idx);
339 if (!row && derived.hasContractTopSuggestion &&
340 !derived.contractTopSuggestion.entityId.empty()) {
342 rowIndexById(contractChoices, derived.contractTopSuggestion.entityId);
343 row = rowByIndex(contractChoices, idx);
346 std::string suggestion;
348 const auto n = !row->name.empty() ? row->name : row->display;
349 const auto t = row->type;
350 if (!n.empty() && !t.empty())
351 suggestion =
"(" + n +
", " + t +
")";
356 }
else if (derived.hasContractTopSuggestion) {
357 suggestion = derived.contractTopSuggestion.label;
360 if (suggestion.empty())
361 return std::to_string(confidencePercent) +
"% Confidence - No suggestion";
362 return std::to_string(confidencePercent) +
"% Confidence - " + suggestion;
365std::vector<std::string>
366propertyAutoSelectIds(
const DraftLinkSelection &selection,
367 const std::vector<DraftChoiceRow> &properties,
368 const std::vector<DraftChoiceRow> &contracts,
369 const std::string &selectedContractId) {
370 std::vector<std::string> ids;
371 const std::string source = !policy::trim(selection.propertyText).empty()
372 ? selection.propertyText
373 : selection.metadata +
" " +
374 selection.actorText +
" " +
376 const auto containsNormalized = [](
const std::string &haystack,
377 const std::string &needle) {
378 const auto h = policy::normalizeText(haystack);
379 const auto n = policy::normalizeText(needle);
380 return !h.empty() && !n.empty() && h.find(n) != std::string::npos;
383 for (
const auto &row : properties) {
386 if (containsNormalized(source, row.name) ||
387 containsNormalized(source, row.display)) {
388 appendUnique(ids, row.id);
391 for (
const auto &alias : row.aliases) {
392 if (containsNormalized(source, alias)) {
393 appendUnique(ids, row.id);
399 for (
const auto &suggestion : selection.propertySuggestions.candidates) {
400 if (suggestion.confidence < 0.25)
402 for (
const auto &row : properties) {
405 if (row.id == suggestion.entityId ||
406 containsNormalized(suggestion.label, row.name) ||
407 containsNormalized(suggestion.label, row.display)) {
408 appendUnique(ids, row.id);
414 const auto contractId =
415 !selectedContractId.empty() ? selectedContractId : selection.contractId;
416 if (!contractId.empty()) {
417 const auto contractIndex = rowIndexById(contracts, contractId);
418 if (
const auto *contractRow = rowByIndex(contracts, contractIndex)) {
419 for (
const auto &propertyId :
contractRow->propertyIds)
420 appendUnique(ids, propertyId);
427double contractScore(
const DraftLinkSelection &selection,
428 const DraftChoiceRow &row,
429 const std::vector<DraftChoiceRow> &actors,
430 const DraftSuggestionCandidate *topContract,
431 const std::string &actorDisplay) {
433 const auto typeText = policy::trim(selection.type);
434 const auto actorText = policy::trim(selection.actorText).empty()
436 : policy::trim(selection.actorText);
438 if (!typeText.empty()) {
439 if (policy::matchesDraftText(row.type, typeText))
441 if (policy::matchesDraftText(row.name, typeText))
443 if (policy::matchesDraftText(row.display, typeText))
447 if (!actorText.empty()) {
448 for (
const auto &actorId : row.actorIds) {
449 const auto actorIndex = rowIndexById(actors, actorId);
450 const auto *actorRowPtr = rowByIndex(actors, actorIndex);
451 const auto actorName =
452 actorRowPtr ? firstExistingText(*actorRowPtr) : actorId;
453 if (policy::matchesDraftText(actorName, actorText)) {
458 if (policy::matchesDraftText(row.name, actorText) ||
459 policy::matchesDraftText(row.type, actorText))
463 for (
const auto &propertyId : selection.propertyIds) {
464 if (containsId(row.propertyIds, propertyId)) {
470 if (topContract && row.id == topContract->entityId)
475template <
typename EntityRange,
typename LabelFn,
typename TextFn>
476DraftSuggestionBucket buildSuggestionBucket(
const EntityRange &entities,
477 const std::string &entityType,
478 LabelFn &&labelFn, TextFn &&textFn,
479 const std::string &sourceText) {
480 DraftSuggestionBucket bucket;
481 bucket.sourceText = sourceText;
483 const auto sourceTokens = policy::tokens(sourceText);
484 const auto sourceLeadTokens =
485 policy::tokens(policy::leadingText(sourceText, 4));
487 struct RankedSuggestion {
488 DraftSuggestionCandidate value;
490 std::vector<RankedSuggestion> ranked;
491 ranked.reserve(entities.size());
493 for (
const auto &entity : entities) {
497 DraftSuggestionCandidate suggestion;
498 suggestion.entityType = entityType;
499 suggestion.entityId = entity->id();
500 suggestion.label = labelFn(*entity);
501 suggestion.sourceText = sourceText;
503 const auto candidateText = policy::normalizeText(textFn(*entity));
504 const auto candidateTokens = policy::tokens(textFn(*entity));
505 const auto usages = aliasUsages(*entity);
508 std::vector<std::string> matchedAliases;
509 double aliasScore = 0.0;
510 double recencyScore = 0.0;
511 int bestHitCount = 0;
512 std::string bestLastUsedAt;
514 double strongestUsageFactor = 0.0;
515 for (
const auto &usage : usages) {
516 strongestUsageFactor =
517 std::max(strongestUsageFactor,
518 policy::aliasHitWeight(usage.hitCount()) * 0.65 +
519 policy::aliasRecencyWeight(usage.lastUsedAt()) * 0.35);
521 if (strongestUsageFactor <= 0.0)
522 strongestUsageFactor = 0.15;
524 if (!candidateText.empty() &&
527 score += 40.0 + (40.0 * strongestUsageFactor * 0.2);
529 score += policy::tokenOverlapScore(sourceTokens, candidateTokens) *
530 (12.0 + strongestUsageFactor * 4.0);
532 policy::tokenOverlapScore(sourceLeadTokens, candidateTokens) * 18.0;
534 for (
const auto &usage : usages) {
535 const std::string alias = usage.value();
536 const auto aliasNorm = policy::normalizeText(alias);
537 if (aliasNorm.empty())
540 const double hitWeight = policy::aliasHitWeight(usage.hitCount());
541 const double recentWeight =
542 policy::aliasRecencyWeight(usage.lastUsedAt());
543 const double usageFactor = 1.0 + hitWeight + recentWeight * 0.75;
546 score += 80.0 * usageFactor;
547 matchedAliases.push_back(alias);
548 aliasScore += hitWeight;
549 recencyScore += recentWeight;
550 bestHitCount = std::max(bestHitCount, usage.hitCount());
551 if (!usage.lastUsedAt().empty() &&
552 (bestLastUsedAt.empty() || usage.lastUsedAt() > bestLastUsedAt))
553 bestLastUsedAt = usage.lastUsedAt();
557 const auto aliasTokens = policy::tokens(alias);
558 const int aliasOverlap =
559 policy::tokenOverlapScore(sourceTokens, aliasTokens);
560 if (aliasOverlap > 0) {
561 score += (10.0 * aliasOverlap) *
562 (1.0 + hitWeight * 0.5 + recentWeight * 0.25);
563 matchedAliases.push_back(alias);
564 aliasScore += hitWeight * 0.5;
565 recencyScore += recentWeight * 0.5;
566 bestHitCount = std::max(bestHitCount, usage.hitCount());
567 if (!usage.lastUsedAt().empty() &&
568 (bestLastUsedAt.empty() || usage.lastUsedAt() > bestLastUsedAt))
569 bestLastUsedAt = usage.lastUsedAt();
575 suggestion.rationale =
"Fallback";
578 suggestion.score = score;
579 suggestion.confidence = std::clamp(score / 220.0, 0.0, 1.0);
580 suggestion.aliasWeight = std::clamp(aliasScore, 0.0, 1.0);
581 suggestion.recencyWeight = std::clamp(recencyScore, 0.0, 1.0);
582 suggestion.hitCount = bestHitCount;
583 suggestion.lastUsedAt = bestLastUsedAt;
584 suggestion.matchedAliases = matchedAliases;
585 if (!matchedAliases.empty()) {
586 suggestion.rationale =
"Alias match";
587 }
else if (!candidateText.empty() &&
590 suggestion.rationale =
"Name match";
592 suggestion.rationale =
"Token overlap";
595 ranked.push_back({std::move(suggestion)});
598 std::sort(ranked.begin(), ranked.end(),
599 [](
const RankedSuggestion &lhs,
const RankedSuggestion &rhs) {
600 if (lhs.value.score == rhs.value.score) {
601 if (lhs.value.confidence == rhs.value.confidence)
602 return lhs.value.label < rhs.value.label;
603 return lhs.value.confidence > rhs.value.confidence;
605 return lhs.value.score > rhs.value.score;
608 constexpr std::size_t kMaxCandidates = 5;
609 bucket.candidates.reserve(std::min(kMaxCandidates, ranked.size()));
610 for (std::size_t i = 0; i < ranked.size() && i < kMaxCandidates; ++i) {
611 bucket.candidates.push_back(std::move(ranked[i].value));
622 auto effectiveSelection = selection;
623 if (policy::trim(effectiveSelection.type).empty() &&
624 !policy::trim(effectiveSelection.metadata).empty()) {
625 const auto metadataLines = policy::splitLines(effectiveSelection.metadata);
626 effectiveSelection.type = policy::extractTypeText(
627 state, effectiveSelection.metadata, metadataLines);
631 const auto actors = actorRows(state);
632 const auto properties = propertyRows(state);
633 const auto contracts = contractRows(state);
638 const auto *actorTop = topSuggestion(effectiveSelection.actorSuggestions);
639 const auto *propertyTop =
640 topSuggestion(effectiveSelection.propertySuggestions);
641 const auto *contractTop =
642 topSuggestion(effectiveSelection.contractSuggestions);
657 actorDisplayText(effectiveSelection, actors, actorTop);
659 contractDisplayText(effectiveSelection, contracts, contractTop);
665 propertySuggestionSummary(effectiveSelection.propertySuggestions);
669 newActor.
name =
"New Actor";
670 newActor.
display =
"New Actor";
672 newActor.
confidence = actorTop ? actorTop->confidence : kSyntheticConfidence;
675 std::move(newActor));
677 std::vector<std::pair<double, DraftChoiceRow>> scoredContracts;
678 scoredContracts.reserve(contracts.size());
679 for (
const auto &row : contracts) {
680 scoredContracts.push_back(
681 {contractScore(effectiveSelection, row, actors, contractTop,
685 std::sort(scoredContracts.begin(), scoredContracts.end(),
686 [](
const auto &lhs,
const auto &rhs) {
687 if (lhs.first == rhs.first)
688 return lhs.second.display < rhs.second.display;
689 return lhs.first > rhs.first;
693 newContract.
name =
"New Contract";
694 newContract.
display =
"New Contract";
698 contractTop ? contractTop->confidence : kSyntheticConfidence;
701 for (
const auto &[score, row] : scoredContracts) {
706 if (!effectiveSelection.actorId.empty()) {
708 rowIndexById(derived.
actorChoices, effectiveSelection.actorId);
709 }
else if (effectiveSelection.actorSelected) {
711 }
else if (actorTop && !actorTop->entityId.empty() &&
712 actorTop->confidence >= 0.2) {
716 if (
const auto *match = findRowByText(actors, derived.
actorSeedText)) {
723 if (!effectiveSelection.contractId.empty()) {
726 }
else if (effectiveSelection.contractSelected) {
728 }
else if (!scoredContracts.empty() &&
729 scoredContracts.front().first >= 900.0) {
733 (!policy::trim(effectiveSelection.actorText).empty() ||
734 !effectiveSelection.actorId.empty() ||
735 !effectiveSelection.propertyIds.empty())) {
736 if (
const auto *match =
745 std::string selectedContractId = effectiveSelection.contractId;
747 if (
const auto *contractRow =
749 contractRow && !contractRow->synthetic) {
750 selectedContractId = contractRow->id;
755 const auto &best = scoredContracts.front();
756 if (!best.second.id.empty()) {
760 !best.second.display.empty()
761 ? best.second.display
762 : (!best.second.name.empty() ? best.second.name
767 std::clamp(best.first / 220.0, 0.0, 1.0);
789 const auto suggestedIndex = rowIndexById(
791 if (suggestedIndex > 0)
795 const std::string effectiveContractId = !selectedContractId.empty()
797 : effectiveSelection.contractId;
798 if (!effectiveContractId.empty()) {
799 for (
const auto &scored : scoredContracts) {
800 if (scored.second.id == effectiveContractId) {
801 const auto scoredConfidence =
802 std::clamp(scored.first / 220.0, 0.0, 1.0);
808 }
else if (!scoredContracts.empty()) {
809 const auto scoredConfidence =
810 std::clamp(scoredContracts.front().first / 220.0, 0.0, 1.0);
816 const auto suggestedIndex = rowIndexById(contracts, effectiveContractId);
817 if (
const auto *suggestedRow = rowByIndex(contracts, suggestedIndex)) {
819 const auto typeText = policy::trim(effectiveSelection.type);
820 if (!typeText.empty() &&
821 policy::matchesDraftText(suggestedRow->type, typeText))
823 if (!effectiveSelection.actorId.empty() &&
824 containsId(suggestedRow->actorIds, effectiveSelection.actorId))
826 if (hasAnyPropertyOverlap(effectiveSelection.propertyIds,
827 suggestedRow->propertyIds))
837 const auto effectiveContractIndex =
838 rowIndexById(contracts, effectiveContractId);
839 const auto *effectiveContractRow =
840 rowByIndex(contracts, effectiveContractIndex);
841 const auto mode = normalizeAllocatableMode(
842 effectiveContractRow ? effectiveContractRow->allocatableMode
844 if (effectiveSelection.allocatableSelected) {
846 }
else if (mode ==
"allocatable") {
848 }
else if (mode ==
"non-allocatable") {
853 state, effectiveContractId) ||
854 effectiveSelection.allocatable;
857 if (mode ==
"allocatable" || mode ==
"non-allocatable") {
865 actorSuggestionSummary(effectiveSelection.actorSuggestions);
867 propertySuggestionSummary(effectiveSelection.propertySuggestions);
870 const auto allocConfidence =
872 const auto allocPercent =
873 static_cast<int>(std::round(allocConfidence * 100.0));
874 if (allocConfidence < kSuggestionYellowThreshold) {
876 std::to_string(allocPercent) +
"% Confidence - No suggestion";
879 std::to_string(allocPercent) +
"% Confidence - " +
884 effectiveSelection, properties, contracts, selectedContractId);
894 const auto actorSourceText = policy::trim(
895 policy::joinNonEmptyLines({signals.actorText, signals.sharedText},
" "));
896 const auto &propertySourceText = signals.propertyText;
897 const auto contractSourceText = signals.sharedText;
900 suggestions.
actor = buildSuggestionBucket(
906 suggestions.
property = buildSuggestionBucket(
912 suggestions.
contract = buildSuggestionBucket(
915 if (contract.name().empty())
916 return contract.type();
917 if (contract.type().empty())
918 return contract.name();
919 return contract.type() +
" — " + contract.name();
922 return contract.type().empty()
924 : contract.type() +
" " + contract.name();
933template <
typename TTarget,
typename TValue>
934bool assignIfChanged(TTarget& target, TValue&& value) {
935 TTarget next = [&]() {
936 if constexpr (std::is_enum_v<TTarget> &&
937 std::is_integral_v<std::remove_cvref_t<TValue>>) {
938 return static_cast<TTarget
>(value);
940 return TTarget{std::forward<TValue>(value)};
943 if (target == next) {
946 target = std::move(next);
950struct DraftAutoSelectionPolicy {
951 double actorConfidenceThreshold = -1.0;
952 double contractConfidenceThreshold = -1.0;
953 bool actorTextFromChoice =
true;
954 bool actorSeedRequiresEmptyText =
true;
955 bool contractTypeRequiresEmptyDraftType =
true;
956 bool contractPropertiesRequireEmptyDraftProperties =
true;
957 bool contractSelected =
false;
958 bool updateActorSelectedFromContract =
false;
959 bool actorSelectedFromContract =
false;
962DraftAutoSelectionPolicy policyFor(DraftAutoSelectionMode mode) {
963 if (mode == DraftAutoSelectionMode::InteractiveSync) {
964 return {.actorConfidenceThreshold = 0.9,
965 .contractConfidenceThreshold = 0.9,
966 .actorTextFromChoice =
false,
967 .actorSeedRequiresEmptyText =
false,
968 .contractTypeRequiresEmptyDraftType =
false,
969 .contractPropertiesRequireEmptyDraftProperties =
false,
970 .contractSelected =
true,
971 .updateActorSelectedFromContract =
true,
972 .actorSelectedFromContract =
false};
977bool passesConfidenceGate(
double threshold,
bool hasSuggestion,
979 return threshold < 0.0 || (hasSuggestion && confidence >= threshold);
982std::string choiceDisplayText(
const DraftChoiceRow& row) {
983 if (!row.display.empty()) {
986 if (!row.name.empty()) {
989 if (!row.type.empty()) {
995const DraftChoiceRow* choiceAt(
const std::vector<DraftChoiceRow>& rows,
997 if (index <= 0 ||
static_cast<std::size_t
>(index) >= rows.size()) {
1000 const auto& row = rows[
static_cast<std::size_t
>(index)];
1001 return !row.synthetic && !row.id.empty() ? &row :
nullptr;
1004const DraftChoiceRow* findChoiceRowById(
const std::vector<DraftChoiceRow>& rows,
1005 const std::string&
id) {
1009 for (
const auto& row : rows) {
1018 const DraftDerivedState& derived,
1019 const DraftAutoSelectionPolicy& policy) {
1024 bool changed =
false;
1025 const double actorConfidence =
1026 derived.hasActorTopSuggestion ? derived.actorTopSuggestion.confidence
1029 choiceAt(derived.actorChoices, derived.actorCurrentIndex);
1031 passesConfidenceGate(policy.actorConfidenceThreshold,
1032 derived.hasActorTopSuggestion, actorConfidence)) {
1034 changed |= assignIfChanged(
1036 policy.actorTextFromChoice ? choiceDisplayText(*actorRow)
1042 if (!derived.actorSeedText.empty() &&
1043 (!policy.actorSeedRequiresEmptyText || draft.
actorText.empty())) {
1044 changed |= assignIfChanged(draft.
actorText, derived.actorSeedText);
1050 const DraftDerivedState& derived,
1051 const DraftAutoSelectionPolicy& policy) {
1056 bool changed =
false;
1058 choiceAt(derived.contractChoices, derived.contractCurrentIndex);
1060 passesConfidenceGate(policy.contractConfidenceThreshold,
1061 derived.hasContractTopSuggestion,
1062 derived.contractSuggestionConfidence)) {
1065 (!policy.contractTypeRequiresEmptyDraftType || draft.
type.empty())) {
1070 if (
const auto* actorRow =
1071 findChoiceRowById(derived.actorChoices,
1073 changed |= assignIfChanged(draft.
actorText,
1074 choiceDisplayText(*actorRow));
1076 if (policy.updateActorSelectedFromContract) {
1078 policy.actorSelectedFromContract);
1082 (!policy.contractPropertiesRequireEmptyDraftProperties ||
1091 if (draft.
type.empty() && !derived.contractSeedText.empty()) {
1092 changed |= assignIfChanged(draft.
type, derived.contractSeedText);
1102 bool changed =
false;
1103 const DraftAutoSelectionPolicy policy = policyFor(mode);
1108 if (
draft.contractId.empty() && !
draft.contractSelected) {
1109 changed |= assignIfChanged(
draft.type, std::string{});
1117 if (!
draft.allocatableSelected) {
1118 changed |= assignIfChanged(
draft.allocatable,
1126 if (actorId.empty()) {
1130 bool changed =
false;
1131 changed |= assignIfChanged(
draft.actorId, actorId);
1132 changed |= assignIfChanged(
draft.actorText, std::string{});
1133 changed |= assignIfChanged(
draft.actorSelected,
true);
1134 changed |= assignIfChanged(
draft.contractId, std::string{});
1135 changed |= assignIfChanged(
draft.contractSelected,
false);
1140 bool changed =
false;
1141 changed |= assignIfChanged(
draft.actorId, std::string{});
1142 changed |= assignIfChanged(
draft.actorText, std::string{});
1143 changed |= assignIfChanged(
draft.actorSelected,
false);
1144 changed |= assignIfChanged(
draft.contractId, std::string{});
1145 changed |= assignIfChanged(
draft.contractSelected,
false);
1150 const std::string& propertyId) {
1151 if (propertyId.empty()) {
1155 bool changed =
false;
1156 if (std::find(
draft.propertyIds.begin(),
draft.propertyIds.end(),
1157 propertyId) ==
draft.propertyIds.end()) {
1158 draft.propertyIds.push_back(propertyId);
1161 changed |= assignIfChanged(
draft.contractId, std::string{});
1162 changed |= assignIfChanged(
draft.contractSelected,
false);
1167 const std::string& propertyId,
1169 if (propertyId.empty()) {
1173 auto propertyIds =
draft.propertyIds;
1175 if (std::find(propertyIds.begin(), propertyIds.end(), propertyId) ==
1176 propertyIds.end()) {
1177 propertyIds.push_back(propertyId);
1180 propertyIds.erase(std::remove(propertyIds.begin(), propertyIds.end(),
1185 bool changed =
false;
1186 changed |= assignIfChanged(
draft.propertyIds, propertyIds);
1188 changed |= assignIfChanged(
draft.contractId, std::string{});
1189 changed |= assignIfChanged(
draft.contractSelected,
false);
1196 if (contract.
id.empty()) {
1200 bool changed =
false;
1201 changed |= assignIfChanged(
draft.contractId, contract.
id);
1202 changed |= assignIfChanged(
draft.contractSelected,
true);
1203 if (!contract.
type.empty()) {
1204 changed |= assignIfChanged(
draft.type, contract.
type);
1207 changed |= assignIfChanged(
draft.actorId, contract.
actorIds.front());
1208 changed |= assignIfChanged(
draft.actorText, std::string{});
1209 changed |= assignIfChanged(
draft.actorSelected,
false);
1216 if (mode ==
"allocatable") {
1217 changed |= assignIfChanged(
draft.allocatable,
true);
1218 changed |= assignIfChanged(
draft.allocatableSelected,
false);
1219 }
else if (mode ==
"non-allocatable") {
1220 changed |= assignIfChanged(
draft.allocatable,
false);
1221 changed |= assignIfChanged(
draft.allocatableSelected,
false);
1228 const std::string& contractId) {
1229 if (contractId.empty()) {
1232 for (
const auto& contract : contractRows(state)) {
1233 if (contract.id == contractId) {
1241 bool changed =
false;
1242 changed |= assignIfChanged(
draft.contractId, std::string{});
1243 changed |= assignIfChanged(
draft.contractSelected,
false);
1249 bool changed =
false;
1251 changed |= assignIfChanged(
draft.name, patch.
name);
1257 changed |= assignIfChanged(
draft.valuta, patch.
valuta);
1260 changed |= assignIfChanged(
draft.amount, patch.
amount);
1263 changed |= assignIfChanged(
1269 changed |= assignIfChanged(
draft.allocatableSelected,
1276 const int newIndex = std::clamp(currentIndex + 1, 0,
1277 static_cast<int>(
draft.transactions.size()));
1279 transaction.id =
"tx-new-" + std::to_string(
draft.transactions.size() + 1);
1282 transaction.name =
"Transaction " + std::to_string(
draft.transactions.size() + 1);
1284 draft.transactions.insert(
draft.transactions.begin() + newIndex,
1286 draft.transactionIds.clear();
1287 draft.transactionIds.reserve(
draft.transactions.size());
1288 for (std::size_t i = 0; i <
draft.transactions.size(); ++i) {
1289 draft.transactions[i].position =
static_cast<int>(i);
1290 draft.transactions[i].statementDraftId =
draft.id;
1291 draft.transactionIds.push_back(
draft.transactions[i].id);
1297 if (
draft.transactions.size() <= 1 || index < 0 ||
1298 static_cast<std::size_t
>(index) >=
draft.transactions.size()) {
1302 draft.transactions.erase(
draft.transactions.begin() + index);
1303 draft.transactionIds.clear();
1304 draft.transactionIds.reserve(
draft.transactions.size());
1305 for (std::size_t i = 0; i <
draft.transactions.size(); ++i) {
1306 draft.transactions[i].position =
static_cast<int>(i);
1307 draft.transactions[i].statementDraftId =
draft.id;
1308 draft.transactionIds.push_back(
draft.transactions[i].id);
1310 return std::min(index,
static_cast<int>(
draft.transactions.size()) - 1);
1314 return assignIfChanged(
draft.name, name);
Declares import-draft matching helpers shared by the import pipeline and draft finalization.
Declares draft matching policy helpers for import-time bank statement inference.
std::vector< std::string > propertyIds
const std::string & name() const noexcept
const PropertyList & properties() const noexcept
Returns all properties in the catalog.
const ActorList & actors() const noexcept
Returns all actors in the catalog.
const ContractList & contracts() const noexcept
Returns all contracts in the catalog.
Collects common core includes and aliases for precompiled-header builds.
bool setPropertySelected(TransactionDraft &draft, const std::string &propertyId, bool selected)
bool contractIsFullyAllocatable(const core::domain::catalog::WorkspaceCatalog &state, const std::string &contractId)
Checks whether all transactions assigned to a contract are allocatable.
bool applyDerivedSelections(TransactionDraft &draft, const DraftDerivedState &derived, DraftAutoSelectionMode mode)
int insertTransactionAfter(StatementDraft &draft, int currentIndex)
DraftDerivedState buildDraftDerivedState(const core::domain::catalog::WorkspaceCatalog &state, const DraftLinkSelection &selection)
Derives UI-agnostic matching state for the currently edited draft link.
bool applyContractSelection(TransactionDraft &draft, const DraftChoiceRow &contract)
bool clearContractSelection(TransactionDraft &draft)
DraftTextSignals buildDraftTextSignals(const core::domain::catalog::WorkspaceCatalog &state, const TransactionDraft &transaction)
Derives text signals from a workspace transaction draft.
bool applyActorSelection(TransactionDraft &draft, const std::string &actorId)
bool renameStatementDraft(StatementDraft &draft, const std::string &name)
int removeTransactionAt(StatementDraft &draft, int index)
DraftImportSuggestions buildImportSuggestions(const core::domain::catalog::WorkspaceCatalog &state, const TransactionDraft &transaction)
Builds ranked import suggestions for the given workspace and transaction draft.
std::string normalizeDraftText(const std::string &text)
Normalizes free-form draft text for fuzzy matching.
bool applyPropertySelection(TransactionDraft &draft, const std::string &propertyId)
bool clearActorSelection(TransactionDraft &draft)
bool applyTransactionPatch(TransactionDraft &draft, const TransactionDraftPatch &patch)
QVariantMap contractRow(const core::ports::workspace::ContractSnapshot &contract)
Builds a contract row.
QVariantMap propertyRow(const core::ports::workspace::PropertySnapshot &property)
Builds a property row.
QVariantMap actorRow(const core::ports::workspace::ActorSnapshot &actor)
Builds an actor row.
Represents a selectable catalog row for import-draft assignment UIs.
std::vector< std::string > propertyIds
std::string allocatableMode
std::vector< std::string > actorIds
Contains all derived matching state needed to render the current transaction draft.
bool hasPropertyTopSuggestion
std::string propertySuggestionSummary
double allocatableSuggestionConfidence
std::string actorSeedText
double actorSuggestionConfidence
std::vector< std::string > autoPropertyIds
DraftSuggestionCandidate contractTopSuggestion
std::vector< DraftChoiceRow > propertyRows
bool hasContractTopSuggestion
std::vector< DraftChoiceRow > contractChoices
bool effectiveAllocatable
std::string contractSeedText
std::vector< DraftChoiceRow > actorChoices
DraftSuggestionCandidate actorTopSuggestion
DraftSuggestionCandidate propertyTopSuggestion
std::string actorSuggestionSummary
std::string allocatableSuggestionSummary
std::string contractSuggestionSummary
double contractSuggestionConfidence
bool hasActorTopSuggestion
std::string actorDisplayText
std::string contractDisplayText
double propertySuggestionConfidence
Holds core-native ranked suggestions for the import draft.
DraftSuggestionBucket property
DraftSuggestionBucket actor
DraftSuggestionBucket contract
Describes the editable transaction-draft link state independent of any UI toolkit.