FOSSredder 0.5.0
Loading...
Searching...
No Matches
DraftMatcherProjection.cpp
Go to the documentation of this file.
1
6
9#include "core/pch.h"
10
11#include <algorithm>
12#include <cctype>
13#include <cmath>
14#include <memory>
15#include <string>
16#include <type_traits>
17#include <utility>
18#include <vector>
19
21
22namespace policy = core::domain::policies::matching;
23
24namespace {
25
26constexpr double kSyntheticConfidence = 0.35;
27constexpr double kSuggestionYellowThreshold = 0.4;
28constexpr double kSuggestionGreenThreshold = 0.9;
29
30std::string normalizeAllocatableMode(std::string value) {
31 auto normalized = policy::trim(std::move(value));
32 std::transform(
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";
37 return normalized;
38}
39
40bool containsId(const std::vector<std::string> &ids, const std::string &id) {
41 return std::find(ids.begin(), ids.end(), id) != ids.end();
42}
43
44bool appendUnique(std::vector<std::string> &values, const std::string &value) {
45 if (value.empty() || containsId(values, value))
46 return false;
47 values.push_back(value);
48 return true;
49}
50
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())
57 continue;
58 out.push_back(alias);
59 }
60 return out;
61}
62
63const DraftSuggestionCandidate *
64topSuggestion(const DraftSuggestionBucket &bucket) {
65 return bucket.candidates.empty() ? nullptr : &bucket.candidates.front();
66}
67
68int confidencePercent(const DraftSuggestionCandidate *suggestion) {
69 return suggestion ? static_cast<int>(std::round(
70 std::clamp(suggestion->confidence, 0.0, 1.0) * 100.0))
71 : 0;
72}
73
74std::string actorDisplay(const core::domain::Actor &actor) {
75 return actor.name();
76}
77
78std::string propertyDisplay(const core::domain::Property &property) {
79 return property.name();
80}
81
82DraftChoiceRow actorRow(const std::shared_ptr<core::domain::Actor> &actor) {
83 DraftChoiceRow row;
84 if (!actor)
85 return row;
86 row.id = actor->id();
87 row.name = actor->name();
88 row.display = actorDisplay(*actor);
89 for (const auto &alias : actor->aliases()) {
90 row.aliases.push_back(alias.value());
91 }
92 return row;
93}
94
95DraftChoiceRow
96propertyRow(const std::shared_ptr<core::domain::Property> &property) {
97 DraftChoiceRow row;
98 if (!property)
99 return row;
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());
105 }
106 return row;
107}
108
109DraftChoiceRow
110contractRow(const std::shared_ptr<core::domain::Contract> &contract) {
111 DraftChoiceRow row;
112 if (!contract)
113 return row;
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());
121 }
122 row.actorIds = contract->actorIds();
123 row.propertyIds = contract->propertyIds();
124 return row;
125}
126
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))
131 return true;
132 }
133 return false;
134}
135
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()) {
141 auto row = actorRow(actor);
142 if (!row.id.empty())
143 rows.push_back(std::move(row));
144 }
145 return rows;
146}
147
148std::vector<DraftChoiceRow>
149propertyRows(const core::domain::catalog::WorkspaceCatalog &state) {
150 std::vector<DraftChoiceRow> rows;
151 rows.reserve(state.properties().size());
152 for (const auto &property : state.properties()) {
153 auto row = propertyRow(property);
154 if (!row.id.empty())
155 rows.push_back(std::move(row));
156 }
157 return rows;
158}
159
160std::vector<DraftChoiceRow>
161contractRows(const core::domain::catalog::WorkspaceCatalog &state) {
162 std::vector<DraftChoiceRow> rows;
163 rows.reserve(state.contracts().size());
164 for (const auto &contract : state.contracts()) {
165 auto row = contractRow(contract);
166 if (!row.id.empty())
167 rows.push_back(std::move(row));
168 }
169 return rows;
170}
171
172int rowIndexById(const std::vector<DraftChoiceRow> &rows,
173 const std::string &id) {
174 if (id.empty())
175 return -1;
176 for (std::size_t i = 0; i < rows.size(); ++i) {
177 if (rows[i].id == id)
178 return static_cast<int>(i);
179 }
180 return -1;
181}
182
183const DraftChoiceRow *rowByIndex(const std::vector<DraftChoiceRow> &rows,
184 int index) {
185 return (index >= 0 && static_cast<std::size_t>(index) < rows.size())
186 ? &rows[static_cast<std::size_t>(index)]
187 : nullptr;
188}
189
190std::string firstExistingText(const DraftChoiceRow &row) {
191 if (!row.display.empty())
192 return row.display;
193 if (!row.name.empty())
194 return row.name;
195 if (!row.type.empty())
196 return row.type;
197 return row.id;
198}
199
200bool rowMatchesText(const DraftChoiceRow &row, const std::string &text) {
201 const auto key = policy::normalizeText(text);
202 if (key.empty())
203 return false;
204
205 if (policy::normalizeText(row.name) == key)
206 return true;
207 if (policy::normalizeText(row.display) == key)
208 return true;
209 if (policy::normalizeText(row.type) == key)
210 return true;
211 for (const auto &alias : row.aliases) {
212 if (policy::normalizeText(alias) == key)
213 return true;
214 }
215 return false;
216}
217
218const DraftChoiceRow *findRowByText(const std::vector<DraftChoiceRow> &rows,
219 const std::string &text) {
220 for (const auto &row : rows) {
221 if (rowMatchesText(row, text))
222 return &row;
223 }
224 return nullptr;
225}
226
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;
234 }
235 if (top && !top->label.empty())
236 return top->label;
237 const auto actorText = policy::trim(selection.actorText);
238 if (!actorText.empty())
239 return actorText;
240 return {};
241}
242
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);
249 if (row) {
250 if (!row->name.empty())
251 return row->name;
252 return firstExistingText(*row);
253 }
254 return selection.contractId;
255 }
256 (void)top;
257 return {};
258}
259
260std::string actorSeedText(const DraftLinkSelection &selection,
261 const std::string &displayText,
262 const DraftSuggestionCandidate *top) {
263 if (!selection.actorId.empty())
264 return displayText;
265 const auto actorText = policy::trim(selection.actorText);
266 if (selection.actorSelected && actorText.empty())
267 return {};
268 if (!actorText.empty())
269 return actorText;
270 return top ? top->label : std::string{};
271}
272
273std::string contractSeedText(const DraftLinkSelection &selection,
274 const std::string &displayText,
275 const DraftSuggestionCandidate *top) {
276 if (!selection.contractId.empty())
277 return displayText;
278 const auto type = policy::trim(selection.type);
279 if (selection.contractSelected && type.empty())
280 return {};
281 if (!type.empty())
282 return type;
283 return {};
284}
285
286std::string propertySuggestionSummary(const DraftSuggestionBucket &bucket) {
287 const auto *top = topSuggestion(bucket);
288 if (!top)
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";
293 }
294
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);
301 }
302
303 if (labels.empty())
304 return std::to_string(pct) + "% Confidence - No suggestion";
305 return std::to_string(pct) + "% Confidence - (" +
306 policy::joinNonEmptyLines(labels, ", ") + ")";
307}
308
309std::string actorSuggestionSummary(const DraftSuggestionBucket &bucket) {
310 const auto *top = topSuggestion(bucket);
311 if (!top)
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";
316 }
317 if (top->label.empty())
318 return std::to_string(pct) + "% Confidence - No suggestion";
319 return std::to_string(pct) + "% Confidence - " + top->label;
320}
321
322std::string
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";
332 }
333
334 const DraftChoiceRow *row = nullptr;
335 if (!selectedContractId.empty()) {
336 const auto idx = rowIndexById(contractChoices, selectedContractId);
337 row = rowByIndex(contractChoices, idx);
338 }
339 if (!row && derived.hasContractTopSuggestion &&
340 !derived.contractTopSuggestion.entityId.empty()) {
341 const auto idx =
342 rowIndexById(contractChoices, derived.contractTopSuggestion.entityId);
343 row = rowByIndex(contractChoices, idx);
344 }
345
346 std::string suggestion;
347 if (row) {
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 + ")";
352 else if (!n.empty())
353 suggestion = n;
354 else
355 suggestion = t;
356 } else if (derived.hasContractTopSuggestion) {
357 suggestion = derived.contractTopSuggestion.label;
358 }
359
360 if (suggestion.empty())
361 return std::to_string(confidencePercent) + "% Confidence - No suggestion";
362 return std::to_string(confidencePercent) + "% Confidence - " + suggestion;
363}
364
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 + " " +
375 selection.type;
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;
381 };
382
383 for (const auto &row : properties) {
384 if (row.id.empty())
385 continue;
386 if (containsNormalized(source, row.name) ||
387 containsNormalized(source, row.display)) {
388 appendUnique(ids, row.id);
389 continue;
390 }
391 for (const auto &alias : row.aliases) {
392 if (containsNormalized(source, alias)) {
393 appendUnique(ids, row.id);
394 break;
395 }
396 }
397 }
398
399 for (const auto &suggestion : selection.propertySuggestions.candidates) {
400 if (suggestion.confidence < 0.25)
401 continue;
402 for (const auto &row : properties) {
403 if (row.id.empty())
404 continue;
405 if (row.id == suggestion.entityId ||
406 containsNormalized(suggestion.label, row.name) ||
407 containsNormalized(suggestion.label, row.display)) {
408 appendUnique(ids, row.id);
409 break;
410 }
411 }
412 }
413
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);
421 }
422 }
423
424 return ids;
425}
426
427double contractScore(const DraftLinkSelection &selection,
428 const DraftChoiceRow &row,
429 const std::vector<DraftChoiceRow> &actors,
430 const DraftSuggestionCandidate *topContract,
431 const std::string &actorDisplay) {
432 double score = 0.0;
433 const auto typeText = policy::trim(selection.type);
434 const auto actorText = policy::trim(selection.actorText).empty()
435 ? actorDisplay
436 : policy::trim(selection.actorText);
437
438 if (!typeText.empty()) {
439 if (policy::matchesDraftText(row.type, typeText))
440 score += 600.0;
441 if (policy::matchesDraftText(row.name, typeText))
442 score += 420.0;
443 if (policy::matchesDraftText(row.display, typeText))
444 score += 360.0;
445 }
446
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)) {
454 score += 520.0;
455 break;
456 }
457 }
458 if (policy::matchesDraftText(row.name, actorText) ||
459 policy::matchesDraftText(row.type, actorText))
460 score += 180.0;
461 }
462
463 for (const auto &propertyId : selection.propertyIds) {
464 if (containsId(row.propertyIds, propertyId)) {
465 score += 160.0;
466 break;
467 }
468 }
469
470 if (topContract && row.id == topContract->entityId)
471 score += 40.0;
472 return score;
473}
474
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;
482
483 const auto sourceTokens = policy::tokens(sourceText);
484 const auto sourceLeadTokens =
485 policy::tokens(policy::leadingText(sourceText, 4));
486
487 struct RankedSuggestion {
488 DraftSuggestionCandidate value;
489 };
490 std::vector<RankedSuggestion> ranked;
491 ranked.reserve(entities.size());
492
493 for (const auto &entity : entities) {
494 if (!entity)
495 continue;
496
497 DraftSuggestionCandidate suggestion;
498 suggestion.entityType = entityType;
499 suggestion.entityId = entity->id();
500 suggestion.label = labelFn(*entity);
501 suggestion.sourceText = sourceText;
502
503 const auto candidateText = policy::normalizeText(textFn(*entity));
504 const auto candidateTokens = policy::tokens(textFn(*entity));
505 const auto usages = aliasUsages(*entity);
506
507 double score = 0.0;
508 std::vector<std::string> matchedAliases;
509 double aliasScore = 0.0;
510 double recencyScore = 0.0;
511 int bestHitCount = 0;
512 std::string bestLastUsedAt;
513
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);
520 }
521 if (strongestUsageFactor <= 0.0)
522 strongestUsageFactor = 0.15;
523
524 if (!candidateText.empty() &&
525 normalizeDraftText(sourceText).find(candidateText) !=
526 std::string::npos) {
527 score += 40.0 + (40.0 * strongestUsageFactor * 0.2);
528 }
529 score += policy::tokenOverlapScore(sourceTokens, candidateTokens) *
530 (12.0 + strongestUsageFactor * 4.0);
531 score +=
532 policy::tokenOverlapScore(sourceLeadTokens, candidateTokens) * 18.0;
533
534 for (const auto &usage : usages) {
535 const std::string alias = usage.value();
536 const auto aliasNorm = policy::normalizeText(alias);
537 if (aliasNorm.empty())
538 continue;
539
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;
544
545 if (normalizeDraftText(sourceText).find(aliasNorm) != std::string::npos) {
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();
554 continue;
555 }
556
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();
570 }
571 }
572
573 if (score <= 0.0) {
574 score = 0.05;
575 suggestion.rationale = "Fallback";
576 }
577
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() &&
588 normalizeDraftText(sourceText).find(candidateText) !=
589 std::string::npos) {
590 suggestion.rationale = "Name match";
591 } else {
592 suggestion.rationale = "Token overlap";
593 }
594
595 ranked.push_back({std::move(suggestion)});
596 }
597
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;
604 }
605 return lhs.value.score > rhs.value.score;
606 });
607
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));
612 }
613
614 return bucket;
615}
616
617} // namespace
618
619DraftDerivedState
621 const DraftLinkSelection &selection) {
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);
628 }
629
630 DraftDerivedState derived;
631 const auto actors = actorRows(state);
632 const auto properties = propertyRows(state);
633 const auto contracts = contractRows(state);
634
635 derived.propertyRows = properties;
636 derived.proofSource.clear();
637
638 const auto *actorTop = topSuggestion(effectiveSelection.actorSuggestions);
639 const auto *propertyTop =
640 topSuggestion(effectiveSelection.propertySuggestions);
641 const auto *contractTop =
642 topSuggestion(effectiveSelection.contractSuggestions);
643 if (actorTop) {
644 derived.actorTopSuggestion = *actorTop;
645 derived.hasActorTopSuggestion = true;
646 }
647 if (propertyTop) {
648 derived.propertyTopSuggestion = *propertyTop;
649 derived.hasPropertyTopSuggestion = true;
650 }
651 if (contractTop) {
652 derived.contractTopSuggestion = *contractTop;
653 derived.hasContractTopSuggestion = true;
654 }
655
656 derived.actorDisplayText =
657 actorDisplayText(effectiveSelection, actors, actorTop);
658 derived.contractDisplayText =
659 contractDisplayText(effectiveSelection, contracts, contractTop);
660 derived.actorSeedText =
661 actorSeedText(effectiveSelection, derived.actorDisplayText, actorTop);
662 derived.contractSeedText = contractSeedText(
663 effectiveSelection, derived.contractDisplayText, contractTop);
665 propertySuggestionSummary(effectiveSelection.propertySuggestions);
666
667 derived.actorChoices = actors;
668 DraftChoiceRow newActor;
669 newActor.name = "New Actor";
670 newActor.display = "New Actor";
671 newActor.synthetic = true;
672 newActor.confidence = actorTop ? actorTop->confidence : kSyntheticConfidence;
673 newActor.sourceText = derived.actorSeedText;
674 derived.actorChoices.insert(derived.actorChoices.begin(),
675 std::move(newActor));
676
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,
682 derived.actorDisplayText),
683 row});
684 }
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;
690 });
691
692 DraftChoiceRow newContract;
693 newContract.name = "New Contract";
694 newContract.display = "New Contract";
695 newContract.type = derived.contractSeedText;
696 newContract.synthetic = true;
697 newContract.confidence =
698 contractTop ? contractTop->confidence : kSyntheticConfidence;
699 newContract.sourceText = derived.contractSeedText;
700 derived.contractChoices.push_back(std::move(newContract));
701 for (const auto &[score, row] : scoredContracts) {
702 (void)score;
703 derived.contractChoices.push_back(row);
704 }
705
706 if (!effectiveSelection.actorId.empty()) {
707 derived.actorCurrentIndex =
708 rowIndexById(derived.actorChoices, effectiveSelection.actorId);
709 } else if (effectiveSelection.actorSelected) {
710 derived.actorCurrentIndex = 0;
711 } else if (actorTop && !actorTop->entityId.empty() &&
712 actorTop->confidence >= 0.2) {
713 derived.actorCurrentIndex =
714 rowIndexById(derived.actorChoices, actorTop->entityId);
715 } else if (!derived.actorSeedText.empty()) {
716 if (const auto *match = findRowByText(actors, derived.actorSeedText)) {
717 derived.actorCurrentIndex = rowIndexById(derived.actorChoices, match->id);
718 }
719 }
720 if (derived.actorCurrentIndex < 0)
721 derived.actorCurrentIndex = derived.actorChoices.empty() ? -1 : 0;
722
723 if (!effectiveSelection.contractId.empty()) {
724 derived.contractCurrentIndex =
725 rowIndexById(derived.contractChoices, effectiveSelection.contractId);
726 } else if (effectiveSelection.contractSelected) {
727 derived.contractCurrentIndex = 0;
728 } else if (!scoredContracts.empty() &&
729 scoredContracts.front().first >= 900.0) {
730 derived.contractCurrentIndex = rowIndexById(
731 derived.contractChoices, scoredContracts.front().second.id);
732 } else if (!derived.contractSeedText.empty() &&
733 (!policy::trim(effectiveSelection.actorText).empty() ||
734 !effectiveSelection.actorId.empty() ||
735 !effectiveSelection.propertyIds.empty())) {
736 if (const auto *match =
737 findRowByText(contracts, derived.contractSeedText)) {
738 derived.contractCurrentIndex =
739 rowIndexById(derived.contractChoices, match->id);
740 }
741 }
742 if (derived.contractCurrentIndex < 0)
743 derived.contractCurrentIndex = derived.contractChoices.empty() ? -1 : 0;
744
745 std::string selectedContractId = effectiveSelection.contractId;
746 if (selectedContractId.empty() && derived.contractCurrentIndex > 0) {
747 if (const auto *contractRow =
748 rowByIndex(derived.contractChoices, derived.contractCurrentIndex);
749 contractRow && !contractRow->synthetic) {
750 selectedContractId = contractRow->id;
751 }
752 }
753
754 if (!derived.hasContractTopSuggestion && !scoredContracts.empty()) {
755 const auto &best = scoredContracts.front();
756 if (!best.second.id.empty()) {
757 derived.contractTopSuggestion.entityId = best.second.id;
758 derived.contractTopSuggestion.entityType = "contract";
760 !best.second.display.empty()
761 ? best.second.display
762 : (!best.second.name.empty() ? best.second.name
763 : best.second.type);
765 derived.contractTopSuggestion.score = best.first;
767 std::clamp(best.first / 220.0, 0.0, 1.0);
768 derived.contractTopSuggestion.rationale = "Derived contract score";
769 derived.hasContractTopSuggestion = true;
770 }
771 }
772
775 ? std::clamp(derived.actorTopSuggestion.confidence, 0.0, 1.0)
776 : 0.0;
779 ? std::clamp(derived.propertyTopSuggestion.confidence, 0.0, 1.0)
780 : 0.0;
783 ? std::clamp(derived.contractTopSuggestion.confidence, 0.0, 1.0)
784 : 0.0;
785
786 if (derived.contractCurrentIndex <= 0 && derived.hasContractTopSuggestion &&
787 !derived.contractTopSuggestion.entityId.empty() &&
788 derived.contractSuggestionConfidence >= kSuggestionGreenThreshold) {
789 const auto suggestedIndex = rowIndexById(
791 if (suggestedIndex > 0)
792 derived.contractCurrentIndex = suggestedIndex;
793 }
794
795 const std::string effectiveContractId = !selectedContractId.empty()
796 ? selectedContractId
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);
804 std::max(derived.contractSuggestionConfidence, scoredConfidence);
805 break;
806 }
807 }
808 } else if (!scoredContracts.empty()) {
809 const auto scoredConfidence =
810 std::clamp(scoredContracts.front().first / 220.0, 0.0, 1.0);
812 std::max(derived.contractSuggestionConfidence, scoredConfidence);
813 }
814
815 if (derived.hasContractTopSuggestion && !effectiveContractId.empty()) {
816 const auto suggestedIndex = rowIndexById(contracts, effectiveContractId);
817 if (const auto *suggestedRow = rowByIndex(contracts, suggestedIndex)) {
818 double boost = 0.0;
819 const auto typeText = policy::trim(effectiveSelection.type);
820 if (!typeText.empty() &&
821 policy::matchesDraftText(suggestedRow->type, typeText))
822 boost += 0.18;
823 if (!effectiveSelection.actorId.empty() &&
824 containsId(suggestedRow->actorIds, effectiveSelection.actorId))
825 boost += 0.2;
826 if (hasAnyPropertyOverlap(effectiveSelection.propertyIds,
827 suggestedRow->propertyIds))
828 boost += 0.12;
829 if (boost > 0.0) {
831 std::clamp(derived.contractSuggestionConfidence +
832 boost * (1.0 - derived.contractSuggestionConfidence),
833 0.0, 1.0);
834 }
835 }
836 }
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
843 : std::string{});
844 if (effectiveSelection.allocatableSelected) {
845 derived.effectiveAllocatable = effectiveSelection.allocatable;
846 } else if (mode == "allocatable") {
847 derived.effectiveAllocatable = true;
848 } else if (mode == "non-allocatable") {
849 derived.effectiveAllocatable = false;
850 } else {
851 derived.effectiveAllocatable =
853 state, effectiveContractId) ||
854 effectiveSelection.allocatable;
855 }
856
857 if (mode == "allocatable" || mode == "non-allocatable") {
858 derived.allocatableSuggestionConfidence = std::clamp(
859 derived.contractSuggestionConfidence * 0.85 + 0.15, 0.0, 1.0);
860 } else {
862 std::clamp(derived.contractSuggestionConfidence * 0.5, 0.0, 1.0);
863 }
864 derived.actorSuggestionSummary =
865 actorSuggestionSummary(effectiveSelection.actorSuggestions);
867 propertySuggestionSummary(effectiveSelection.propertySuggestions);
868 derived.contractSuggestionSummary = contractSuggestionSummary(
869 derived, derived.contractChoices, effectiveContractId);
870 const auto allocConfidence =
871 std::clamp(derived.allocatableSuggestionConfidence, 0.0, 1.0);
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";
877 } else {
879 std::to_string(allocPercent) + "% Confidence - " +
880 (derived.effectiveAllocatable ? "Allocatable" : "Not allocatable");
881 }
882
883 derived.autoPropertyIds = propertyAutoSelectIds(
884 effectiveSelection, properties, contracts, selectedContractId);
885
886 return derived;
887}
888
892 const auto signals = buildDraftTextSignals(state, transaction);
893
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;
898
899 DraftImportSuggestions suggestions;
900 suggestions.actor = buildSuggestionBucket(
901 state.actors(), "actor",
902 [](const core::domain::Actor &actor) { return actor.name(); },
903 [](const core::domain::Actor &actor) { return actor.name(); },
904 actorSourceText);
905
906 suggestions.property = buildSuggestionBucket(
907 state.properties(), "property",
908 [](const core::domain::Property &property) { return property.name(); },
909 [](const core::domain::Property &property) { return property.name(); },
910 propertySourceText);
911
912 suggestions.contract = buildSuggestionBucket(
913 state.contracts(), "contract",
914 [](const core::domain::Contract &contract) {
915 if (contract.name().empty())
916 return contract.type();
917 if (contract.type().empty())
918 return contract.name();
919 return contract.type() + " — " + contract.name();
920 },
921 [](const core::domain::Contract &contract) {
922 return contract.type().empty()
923 ? contract.name()
924 : contract.type() + " " + contract.name();
925 },
926 contractSourceText);
927
928 return suggestions;
929}
930
931namespace {
932
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);
939 } else {
940 return TTarget{std::forward<TValue>(value)};
941 }
942 }();
943 if (target == next) {
944 return false;
945 }
946 target = std::move(next);
947 return true;
948}
949
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;
960};
961
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};
973 }
974 return {};
975}
976
977bool passesConfidenceGate(double threshold, bool hasSuggestion,
978 double confidence) {
979 return threshold < 0.0 || (hasSuggestion && confidence >= threshold);
980}
981
982std::string choiceDisplayText(const DraftChoiceRow& row) {
983 if (!row.display.empty()) {
984 return row.display;
985 }
986 if (!row.name.empty()) {
987 return row.name;
988 }
989 if (!row.type.empty()) {
990 return row.type;
991 }
992 return row.id;
993}
994
995const DraftChoiceRow* choiceAt(const std::vector<DraftChoiceRow>& rows,
996 int index) {
997 if (index <= 0 || static_cast<std::size_t>(index) >= rows.size()) {
998 return nullptr;
999 }
1000 const auto& row = rows[static_cast<std::size_t>(index)];
1001 return !row.synthetic && !row.id.empty() ? &row : nullptr;
1002}
1003
1004const DraftChoiceRow* findChoiceRowById(const std::vector<DraftChoiceRow>& rows,
1005 const std::string& id) {
1006 if (id.empty()) {
1007 return nullptr;
1008 }
1009 for (const auto& row : rows) {
1010 if (row.id == id) {
1011 return &row;
1012 }
1013 }
1014 return nullptr;
1015}
1016
1018 const DraftDerivedState& derived,
1019 const DraftAutoSelectionPolicy& policy) {
1020 if (!draft.actorId.empty()) {
1021 return false;
1022 }
1023
1024 bool changed = false;
1025 const double actorConfidence =
1026 derived.hasActorTopSuggestion ? derived.actorTopSuggestion.confidence
1027 : 0.0;
1028 const auto* actorRow =
1029 choiceAt(derived.actorChoices, derived.actorCurrentIndex);
1030 if (actorRow &&
1031 passesConfidenceGate(policy.actorConfidenceThreshold,
1032 derived.hasActorTopSuggestion, actorConfidence)) {
1033 changed |= assignIfChanged(draft.actorId, actorRow->id);
1034 changed |= assignIfChanged(
1035 draft.actorText,
1036 policy.actorTextFromChoice ? choiceDisplayText(*actorRow)
1037 : std::string{});
1038 changed |= assignIfChanged(draft.actorSelected, false);
1039 return changed;
1040 }
1041
1042 if (!derived.actorSeedText.empty() &&
1043 (!policy.actorSeedRequiresEmptyText || draft.actorText.empty())) {
1044 changed |= assignIfChanged(draft.actorText, derived.actorSeedText);
1045 }
1046 return changed;
1047}
1048
1050 const DraftDerivedState& derived,
1051 const DraftAutoSelectionPolicy& policy) {
1052 if (!draft.contractId.empty()) {
1053 return false;
1054 }
1055
1056 bool changed = false;
1057 const auto* contractRow =
1058 choiceAt(derived.contractChoices, derived.contractCurrentIndex);
1059 if (contractRow &&
1060 passesConfidenceGate(policy.contractConfidenceThreshold,
1061 derived.hasContractTopSuggestion,
1062 derived.contractSuggestionConfidence)) {
1063 changed |= assignIfChanged(draft.contractId, contractRow->id);
1064 if (!contractRow->type.empty() &&
1065 (!policy.contractTypeRequiresEmptyDraftType || draft.type.empty())) {
1066 changed |= assignIfChanged(draft.type, contractRow->type);
1067 }
1068 if (!contractRow->actorIds.empty()) {
1069 changed |= assignIfChanged(draft.actorId, contractRow->actorIds.front());
1070 if (const auto* actorRow =
1071 findChoiceRowById(derived.actorChoices,
1072 contractRow->actorIds.front())) {
1073 changed |= assignIfChanged(draft.actorText,
1074 choiceDisplayText(*actorRow));
1075 }
1076 if (policy.updateActorSelectedFromContract) {
1077 changed |= assignIfChanged(draft.actorSelected,
1078 policy.actorSelectedFromContract);
1079 }
1080 }
1081 if (!contractRow->propertyIds.empty() &&
1082 (!policy.contractPropertiesRequireEmptyDraftProperties ||
1083 draft.propertyIds.empty())) {
1084 changed |= assignIfChanged(draft.propertyIds, contractRow->propertyIds);
1085 }
1086 changed |=
1087 assignIfChanged(draft.contractSelected, policy.contractSelected);
1088 return changed;
1089 }
1090
1091 if (draft.type.empty() && !derived.contractSeedText.empty()) {
1092 changed |= assignIfChanged(draft.type, derived.contractSeedText);
1093 }
1094 return changed;
1095}
1096
1097} // namespace
1098
1100 const DraftDerivedState& derived,
1102 bool changed = false;
1103 const DraftAutoSelectionPolicy policy = policyFor(mode);
1104 changed |= applyActorSelection(draft, derived, policy);
1105 changed |= applyContractSelection(draft, derived, policy);
1106
1108 if (draft.contractId.empty() && !draft.contractSelected) {
1109 changed |= assignIfChanged(draft.type, std::string{});
1110 }
1111 }
1112
1113 if (draft.propertyIds.empty() && !derived.autoPropertyIds.empty()) {
1114 changed |= assignIfChanged(draft.propertyIds, derived.autoPropertyIds);
1115 }
1116
1117 if (!draft.allocatableSelected) {
1118 changed |= assignIfChanged(draft.allocatable,
1119 derived.effectiveAllocatable);
1120 }
1121
1122 return changed;
1123}
1124
1125bool applyActorSelection(TransactionDraft& draft, const std::string& actorId) {
1126 if (actorId.empty()) {
1127 return false;
1128 }
1129
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);
1136 return changed;
1137}
1138
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);
1146 return changed;
1147}
1148
1150 const std::string& propertyId) {
1151 if (propertyId.empty()) {
1152 return false;
1153 }
1154
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);
1159 changed = true;
1160 }
1161 changed |= assignIfChanged(draft.contractId, std::string{});
1162 changed |= assignIfChanged(draft.contractSelected, false);
1163 return changed;
1164}
1165
1167 const std::string& propertyId,
1168 bool selected) {
1169 if (propertyId.empty()) {
1170 return false;
1171 }
1172
1173 auto propertyIds = draft.propertyIds;
1174 if (selected) {
1175 if (std::find(propertyIds.begin(), propertyIds.end(), propertyId) ==
1176 propertyIds.end()) {
1177 propertyIds.push_back(propertyId);
1178 }
1179 } else {
1180 propertyIds.erase(std::remove(propertyIds.begin(), propertyIds.end(),
1181 propertyId),
1182 propertyIds.end());
1183 }
1184
1185 bool changed = false;
1186 changed |= assignIfChanged(draft.propertyIds, propertyIds);
1187 if (changed) {
1188 changed |= assignIfChanged(draft.contractId, std::string{});
1189 changed |= assignIfChanged(draft.contractSelected, false);
1190 }
1191 return changed;
1192}
1193
1195 const DraftChoiceRow& contract) {
1196 if (contract.id.empty()) {
1197 return false;
1198 }
1199
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);
1205 }
1206 if (!contract.actorIds.empty()) {
1207 changed |= assignIfChanged(draft.actorId, contract.actorIds.front());
1208 changed |= assignIfChanged(draft.actorText, std::string{});
1209 changed |= assignIfChanged(draft.actorSelected, false);
1210 }
1211 if (!contract.propertyIds.empty()) {
1212 changed |= assignIfChanged(draft.propertyIds, contract.propertyIds);
1213 }
1214
1215 const auto mode = normalizeDraftText(contract.allocatableMode);
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);
1222 }
1223 return changed;
1224}
1225
1228 const std::string& contractId) {
1229 if (contractId.empty()) {
1230 return false;
1231 }
1232 for (const auto& contract : contractRows(state)) {
1233 if (contract.id == contractId) {
1234 return applyContractSelection(draft, contract);
1235 }
1236 }
1237 return false;
1238}
1239
1241 bool changed = false;
1242 changed |= assignIfChanged(draft.contractId, std::string{});
1243 changed |= assignIfChanged(draft.contractSelected, false);
1244 return changed;
1245}
1246
1248 const TransactionDraftPatch& patch) {
1249 bool changed = false;
1250 if (patch.hasName) {
1251 changed |= assignIfChanged(draft.name, patch.name);
1252 }
1253 if (patch.hasBookingDate) {
1254 changed |= assignIfChanged(draft.bookingDate, patch.bookingDate);
1255 }
1256 if (patch.hasValuta) {
1257 changed |= assignIfChanged(draft.valuta, patch.valuta);
1258 }
1259 if (patch.hasAmount) {
1260 changed |= assignIfChanged(draft.amount, patch.amount);
1261 }
1262 if (patch.hasStatus) {
1263 changed |= assignIfChanged(
1264 draft.status,
1265 static_cast<core::domain::Transaction::Status>(patch.status));
1266 }
1267 if (patch.hasAllocatable) {
1268 changed |= assignIfChanged(draft.allocatable, patch.allocatable);
1269 changed |= assignIfChanged(draft.allocatableSelected,
1270 patch.allocatableSelected);
1271 }
1272 return changed;
1273}
1274
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);
1280 transaction.statementDraftId = draft.id;
1281 transaction.position = newIndex;
1282 transaction.name = "Transaction " + std::to_string(draft.transactions.size() + 1);
1283
1284 draft.transactions.insert(draft.transactions.begin() + newIndex,
1285 std::move(transaction));
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);
1292 }
1293 return newIndex;
1294}
1295
1297 if (draft.transactions.size() <= 1 || index < 0 ||
1298 static_cast<std::size_t>(index) >= draft.transactions.size()) {
1299 return -1;
1300 }
1301
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);
1309 }
1310 return std::min(index, static_cast<int>(draft.transactions.size()) - 1);
1311}
1312
1313bool renameStatementDraft(StatementDraft& draft, const std::string& name) {
1314 return assignIfChanged(draft.name, name);
1315}
1316
1317} // namespace core::application::importing::draft
Declares import-draft matching helpers shared by the import pipeline and draft finalization.
Declares draft matching policy helpers for import-time bank statement inference.
const std::string & name() const noexcept
Definition Actor.cpp:89
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.
Contains all derived matching state needed to render the current transaction draft.
Holds core-native ranked suggestions for the import draft.