Skip to content

Feat/universal aggregate metrics#72

Open
hayleypark wants to merge 20 commits intodevfrom
feat/universal_aggregate_metrics_cherry_pick
Open

Feat/universal aggregate metrics#72
hayleypark wants to merge 20 commits intodevfrom
feat/universal_aggregate_metrics_cherry_pick

Conversation

@hayleypark
Copy link

@hayleypark hayleypark commented Jan 29, 2026

Issue #, if available: #33 & Fix universal aggregate metrics calculation

Description of changes:

  • Updated Hungarian Matching for Structured Lists

    • Updated algorithm to always match single-item lists to count as TP if similarity score exceeds match_threshold
    • Refined logic to ensure matched pairs are always available for aggregate metrics
  • Updated StructuredListComparator

    • When either or both instances for comparison are empty, properly account for child leaf node metrics.
    • Simplified calculating nested field metrics to reuse the structured model's compare recursive logic instead of re-inventing the wheel
  • Dispatcher Logic Refactoring:

    • Added helper method to identify List[StructuredModel] fields specifically (_is_structured_list_field)
    • When a field is a structured list, all value types (including null) are now properly delegated to StructuredListComparator
  • Improved Test Cases for Aggregate Functionality:

    • Update exiting test cases for correct universal aggregate metrics calculation
    • Added a new test cases for universal aggregate metrics calculation
  • Enhanced Bulk Evaluator with Aggregate Support:

    • Added include_aggregates parameter to BulkStructuredModelEvaluator
    • New recall_with_fd parameter for flexible recall calculations
    • Aggregate metrics are accumulated separately from regular confusion matrix metrics
    • Enhanced pretty printing to display both regular and aggregate metrics
  • Assumption: For aggregate metrics, all matched items (regardless of the similarity score) are considered.

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

hayleypark and others added 18 commits January 29, 2026 11:18
…model_evaluator to optionally include aggregate results and optionally use recall with fd
…rics to handle aggregation of good and bad matches. The logic is changed from reliance on _handle_hierarchical_field to compare_recursive in comparison_engine.
…turning the field_details. This will allow the aggregate metrics to correctly trickle up.
1) In comparison_dispatcher.py, inside dispatch_field_comparison function, use handle_list_field_dispatch only when simple list. When it's structured list, let compare_struct_list_with_scores handle structured list.
3) In structured_list_comparator.py, _calculate_nested_field_metrics, simplied the code. No threshold of matched pairs is applied. Field details are calculated for all matched pairs and matched objects.
Note that the following update is no longer applies as the method is removed in dev:
2) In structured_list_comparator.py, in _handle_struct_list_empty_cases only handle when both are empty. Otherwise, run through the logic. This is necessary to get the field level metrics.
… (e.g., when field value is list versus string)

2) Removed checking for _aggregate flag in test_json_schema_field_converter.py

3) One major change in many test files:
* Update incorrect test cases to pass all test cases in test_structured_model_evaluator_nested.py. Some test cases still fail in test_ecommerce_orders_aggregate_comprehensive.py, which require additional code change. Clean up some print statements, unused tests.

* Update comments per feedback

---------

Co-authored-by: Hayley Park <parhyunj@amazon.com>
* Updating the logic in dispatcher:
1) Moving from runtime type checking to static field definition checking. Doing this by moving away from _should_use_hierarchical_structure and using _is_structured_list_field instead. This is done mainly for list fields, both primitive and structured. Future changes could expand to other types as needed.
2) when the field is a structured list, delegate handling all types of values whether null or not, to StructuredListComparator

Updated structured_model:
Add helper method to check if a field is specifically List[StructuredModel], complementing _is_list_field() which checks for any list type.
* Update the early exit logic in Hungarian matching to make sure there is always matched pairs for aggregate metrics

---------

Co-authored-by: Hayley Park <parhyunj@amazon.com>
* Previously when a structured list was empty, it would be counted as one TN for aggregate at the object level but aggregation may miss this (because overall metrics are skipped when there are child instances). This change creates a field dict to capture TN at the child leaf node, so that aggregate metric can trickle up correctly.

2) Updated tests to reflect changes due to above logic
* Update hungarian to better handle single-item lists. Always match single-item lists, count as tp if the item similarity score is higher than match_threshold. Update fn count for a test_hungarian test case

* Remove an unused, commented out variable
…per.is_effectively_null_for_primitives in comparison_dispatcher
@github-actions
Copy link

🔒 Security Scan Results

ASH Security Scan Report

  • Report generated: 2026-01-29T23:45:45+00:00
  • Time since scan: 1 minute

Scan Metadata

  • Project: ASH
  • Scan executed: 2026-01-29T23:43:56+00:00
  • ASH version: 3.1.2

Summary

Scanner Results

The table below shows findings by scanner, with status based on severity thresholds and dependencies:

  • Severity levels:
    • Suppressed (S): Findings that have been explicitly suppressed and don't affect scanner status
    • Critical (C): Highest severity findings that require immediate attention
    • High (H): Serious findings that should be addressed soon
    • Medium (M): Moderate risk findings
    • Low (L): Lower risk findings
    • Info (I): Informational findings with minimal risk
  • Duration (Time): Time taken by the scanner to complete its execution
  • Actionable: Number of findings at or above the threshold severity level that require attention
  • Result:
    • PASSED = No findings at or above threshold
    • FAILED = Findings at or above threshold
    • MISSING = Required dependencies not available
    • SKIPPED = Scanner explicitly disabled
    • ERROR = Scanner execution error
  • Threshold: The minimum severity level that will cause a scanner to fail
    • Thresholds: ALL, LOW, MEDIUM, HIGH, CRITICAL
    • Source: Values in parentheses indicate where the threshold is set:
      • global (global_settings section in the ASH_CONFIG used)
      • config (scanner config section in the ASH_CONFIG used)
      • scanner (default configuration in the plugin, if explicitly set)
  • Statistics calculation:
    • All statistics are calculated from the final aggregated SARIF report
    • Suppressed findings are counted separately and do not contribute to actionable findings
    • Scanner status is determined by comparing actionable findings to the threshold
Scanner Suppressed Critical High Medium Low Info Actionable Result Threshold
bandit 0 0 0 0 3217 0 0 PASSED MEDIUM (global)
cdk-nag 0 0 0 0 0 0 0 PASSED MEDIUM (global)
cfn-nag 0 0 0 0 0 0 0 MISSING MEDIUM (global)
checkov 0 1 0 0 0 0 1 SKIPPED MEDIUM (global)
detect-secrets 0 0 0 0 0 0 0 PASSED MEDIUM (global)
grype 0 0 0 0 0 0 0 MISSING MEDIUM (global)
npm-audit 0 0 0 0 0 0 0 PASSED MEDIUM (global)
opengrep 0 0 0 0 0 0 0 MISSING MEDIUM (global)
semgrep 0 1 0 0 0 0 1 FAILED MEDIUM (global)
syft 0 0 0 0 0 0 0 MISSING MEDIUM (global)

Top 2 Hotspots

Files with the highest number of security findings:

Finding Count File Location
1 .github/workflows/workflow.yml
1 src/stickler/reporting/html/interactive/main.js

Detailed Findings

Show 2 actionable findings

Finding 1: CKV2_GHA_1

  • Severity: HIGH
  • Scanner: checkov
  • Rule ID: CKV2_GHA_1
  • Location: .github/workflows/workflow.yml:11-12

Description:
Ensure top-level permissions are not set to write-all


Finding 2: javascript.browser.security.insecure-document-method.insecure-document-method

  • Severity: HIGH
  • Scanner: semgrep
  • Rule ID: javascript.browser.security.insecure-document-method.insecure-document-method
  • Location: src/stickler/reporting/html/interactive/main.js:407-414

Description:
User controlled data in methods like innerHTML, outerHTML or document.write is an anti-pattern that can lead to XSS vulnerabilities

Code Snippet:

pdfItem.innerHTML = `
                <div class="pdf-container">
                    <canvas id="pdf-canvas-${escapeHtml(docId)}" class="pdf-canvas"></canvas>
                    <div class="pdf-loading" id="pdf-loading-${escpeHtml(docId)}">Loading PDF...</div>
                    <div class="pdf-error" id="pdf-error-${escapeHtml(docId)}" style="display: none;">Error loading PDF</div>
                </div>
                <p><strong>${escapeHtml(docId)}</strong></p>
            `;

Report generated by Automated Security Helper (ASH) at 2026-01-29T23:45:46+00:00

@github-actions
Copy link

🔒 Security Scan Results

ASH Security Scan Report

  • Report generated: 2026-01-30T00:20:30+00:00
  • Time since scan: 2 minutes

Scan Metadata

  • Project: ASH
  • Scan executed: 2026-01-30T00:18:29+00:00
  • ASH version: 3.1.2

Summary

Scanner Results

The table below shows findings by scanner, with status based on severity thresholds and dependencies:

  • Severity levels:
    • Suppressed (S): Findings that have been explicitly suppressed and don't affect scanner status
    • Critical (C): Highest severity findings that require immediate attention
    • High (H): Serious findings that should be addressed soon
    • Medium (M): Moderate risk findings
    • Low (L): Lower risk findings
    • Info (I): Informational findings with minimal risk
  • Duration (Time): Time taken by the scanner to complete its execution
  • Actionable: Number of findings at or above the threshold severity level that require attention
  • Result:
    • PASSED = No findings at or above threshold
    • FAILED = Findings at or above threshold
    • MISSING = Required dependencies not available
    • SKIPPED = Scanner explicitly disabled
    • ERROR = Scanner execution error
  • Threshold: The minimum severity level that will cause a scanner to fail
    • Thresholds: ALL, LOW, MEDIUM, HIGH, CRITICAL
    • Source: Values in parentheses indicate where the threshold is set:
      • global (global_settings section in the ASH_CONFIG used)
      • config (scanner config section in the ASH_CONFIG used)
      • scanner (default configuration in the plugin, if explicitly set)
  • Statistics calculation:
    • All statistics are calculated from the final aggregated SARIF report
    • Suppressed findings are counted separately and do not contribute to actionable findings
    • Scanner status is determined by comparing actionable findings to the threshold
Scanner Suppressed Critical High Medium Low Info Actionable Result Threshold
bandit 0 0 0 0 3217 0 0 PASSED MEDIUM (global)
cdk-nag 0 0 0 0 0 0 0 PASSED MEDIUM (global)
cfn-nag 0 0 0 0 0 0 0 MISSING MEDIUM (global)
checkov 0 1 0 0 0 0 1 SKIPPED MEDIUM (global)
detect-secrets 0 0 0 0 0 0 0 PASSED MEDIUM (global)
grype 0 0 0 0 0 0 0 MISSING MEDIUM (global)
npm-audit 0 0 0 0 0 0 0 PASSED MEDIUM (global)
opengrep 0 0 0 0 0 0 0 MISSING MEDIUM (global)
semgrep 0 1 0 0 0 0 1 FAILED MEDIUM (global)
syft 0 0 0 0 0 0 0 MISSING MEDIUM (global)

Top 2 Hotspots

Files with the highest number of security findings:

Finding Count File Location
1 .github/workflows/workflow.yml
1 src/stickler/reporting/html/interactive/main.js

Detailed Findings

Show 2 actionable findings

Finding 1: CKV2_GHA_1

  • Severity: HIGH
  • Scanner: checkov
  • Rule ID: CKV2_GHA_1
  • Location: .github/workflows/workflow.yml:11-12

Description:
Ensure top-level permissions are not set to write-all


Finding 2: javascript.browser.security.insecure-document-method.insecure-document-method

  • Severity: HIGH
  • Scanner: semgrep
  • Rule ID: javascript.browser.security.insecure-document-method.insecure-document-method
  • Location: src/stickler/reporting/html/interactive/main.js:407-414

Description:
User controlled data in methods like innerHTML, outerHTML or document.write is an anti-pattern that can lead to XSS vulnerabilities

Code Snippet:

pdfItem.innerHTML = `
                <div class="pdf-container">
                    <canvas id="pdf-canvas-${escapeHtml(docId)}" class="pdf-canvas"></canvas>
                    <div class="pdf-loading" id="pdf-loading-${escpeHtml(docId)}">Loading PDF...</div>
                    <div class="pdf-error" id="pdf-error-${escapeHtml(docId)}" style="display: none;">Error loading PDF</div>
                </div>
                <p><strong>${escapeHtml(docId)}</strong></p>
            `;

Report generated by Automated Security Helper (ASH) at 2026-01-30T00:20:31+00:00

@github-actions
Copy link

🔒 Security Scan Results

ASH Security Scan Report

  • Report generated: 2026-01-30T00:32:06+00:00
  • Time since scan: 2 minutes

Scan Metadata

  • Project: ASH
  • Scan executed: 2026-01-30T00:30:01+00:00
  • ASH version: 3.1.2

Summary

Scanner Results

The table below shows findings by scanner, with status based on severity thresholds and dependencies:

  • Severity levels:
    • Suppressed (S): Findings that have been explicitly suppressed and don't affect scanner status
    • Critical (C): Highest severity findings that require immediate attention
    • High (H): Serious findings that should be addressed soon
    • Medium (M): Moderate risk findings
    • Low (L): Lower risk findings
    • Info (I): Informational findings with minimal risk
  • Duration (Time): Time taken by the scanner to complete its execution
  • Actionable: Number of findings at or above the threshold severity level that require attention
  • Result:
    • PASSED = No findings at or above threshold
    • FAILED = Findings at or above threshold
    • MISSING = Required dependencies not available
    • SKIPPED = Scanner explicitly disabled
    • ERROR = Scanner execution error
  • Threshold: The minimum severity level that will cause a scanner to fail
    • Thresholds: ALL, LOW, MEDIUM, HIGH, CRITICAL
    • Source: Values in parentheses indicate where the threshold is set:
      • global (global_settings section in the ASH_CONFIG used)
      • config (scanner config section in the ASH_CONFIG used)
      • scanner (default configuration in the plugin, if explicitly set)
  • Statistics calculation:
    • All statistics are calculated from the final aggregated SARIF report
    • Suppressed findings are counted separately and do not contribute to actionable findings
    • Scanner status is determined by comparing actionable findings to the threshold
Scanner Suppressed Critical High Medium Low Info Actionable Result Threshold
bandit 0 0 0 0 3217 0 0 PASSED MEDIUM (global)
cdk-nag 0 0 0 0 0 0 0 PASSED MEDIUM (global)
cfn-nag 0 0 0 0 0 0 0 MISSING MEDIUM (global)
checkov 0 1 0 0 0 0 1 SKIPPED MEDIUM (global)
detect-secrets 0 0 0 0 0 0 0 PASSED MEDIUM (global)
grype 0 0 0 0 0 0 0 MISSING MEDIUM (global)
npm-audit 0 0 0 0 0 0 0 PASSED MEDIUM (global)
opengrep 0 0 0 0 0 0 0 MISSING MEDIUM (global)
semgrep 0 1 0 0 0 0 1 FAILED MEDIUM (global)
syft 0 0 0 0 0 0 0 MISSING MEDIUM (global)

Top 2 Hotspots

Files with the highest number of security findings:

Finding Count File Location
1 .github/workflows/workflow.yml
1 src/stickler/reporting/html/interactive/main.js

Detailed Findings

Show 2 actionable findings

Finding 1: CKV2_GHA_1

  • Severity: HIGH
  • Scanner: checkov
  • Rule ID: CKV2_GHA_1
  • Location: .github/workflows/workflow.yml:11-12

Description:
Ensure top-level permissions are not set to write-all


Finding 2: javascript.browser.security.insecure-document-method.insecure-document-method

  • Severity: HIGH
  • Scanner: semgrep
  • Rule ID: javascript.browser.security.insecure-document-method.insecure-document-method
  • Location: src/stickler/reporting/html/interactive/main.js:407-414

Description:
User controlled data in methods like innerHTML, outerHTML or document.write is an anti-pattern that can lead to XSS vulnerabilities

Code Snippet:

pdfItem.innerHTML = `
                <div class="pdf-container">
                    <canvas id="pdf-canvas-${escapeHtml(docId)}" class="pdf-canvas"></canvas>
                    <div class="pdf-loading" id="pdf-loading-${escpeHtml(docId)}">Loading PDF...</div>
                    <div class="pdf-error" id="pdf-error-${escapeHtml(docId)}" style="display: none;">Error loading PDF</div>
                </div>
                <p><strong>${escapeHtml(docId)}</strong></p>
            `;

Report generated by Automated Security Helper (ASH) at 2026-01-30T00:32:07+00:00

@hayleypark hayleypark marked this pull request as ready for review January 30, 2026 01:34
@hayleypark hayleypark changed the title Feat/universal aggregate metrics cherry pick Feat/universal aggregate metrics Feb 5, 2026
Comment on lines -244 to 249
# items.name should have 1 TP (only item1's name, item2 was below threshold)
# items.name should have 2 TP (both item1 and item2)
# Overall should have some metrics from poor matches at the leaf node level.
name_metrics = items_fields["name"]
if "overall" in name_metrics:
assert name_metrics["overall"]["tp"] == 1
assert name_metrics["overall"]["tp"] == 2
else:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with this, looks correct as written here.

Comment on lines -260 to 269
# items.count should have 1 TP (only item1's count, item2 was below threshold)
# items.count should have 1 TP (only item1's count, item2's count did not pass comparison)
count_metrics = items_fields["count"]
if "overall" in count_metrics:
assert count_metrics["overall"]["tp"] == 1 # item1 count matches
assert (
count_metrics["overall"]["fp"] == 0
) # No false positives since item2 not analyzed at field level
assert count_metrics["overall"]["fd"] == 0 # No false discoveries for count
count_metrics["overall"]["fp"] == 1
) # 1 False positive since item2 matched but count should have been empty
assert count_metrics["overall"]["fa"] == 1 # 1 false alarm for count
else:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with this fix as well.

"Items in EST but not in GT (null) should be False Alarm (FP)"
)
assert items_cm["tn"] == 1, "Both empty lists should be TN"
assert items_cm["tn"] == 1, f"Both empty lists should be TN: {items_cm}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just better assertion comment.

name_metrics = items_fields["name"]
if "overall" in name_metrics:
assert name_metrics["overall"]["tp"] == 1
assert name_metrics["overall"]["tp"] == 2
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, there are two 'name' instances.

Comment on lines -275 to 285
# items.description should have 1 TP (only item1's description, item2 was below threshold)
# items.description should have 1 TP (only item1's description, item2's description did not pass comparison)
desc_metrics = items_fields["description"]
if "overall" in desc_metrics:
assert desc_metrics["overall"]["tp"] == 1 # item1 description matches
assert (
desc_metrics["overall"]["fd"] == 0
) # No false discoveries since item2 not analyzed at field level
assert desc_metrics["overall"]["fp"] == 0 # No false positives for description
desc_metrics["overall"]["fd"] == 1
) # 1 false discoveries since item2 matched but description not correct at field level
assert desc_metrics["overall"]["fp"] == 1 # 1 false positives for description
else:
assert desc_metrics["tp"] == 1 # item1 description matches
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with this assertion too.

Comment on lines -347 to 330
# Direct access to metrics for pet fields (not in "overall")
# petId metrics
assert (
get_metric(cm["fields"]["pets"]["fields"]["petId"], "tp") == 1
), "Expected 1 true positives"
get_metric(cm["fields"]["pets"]["fields"]["petId"], "tp") == 2
), "Expected 2 true positives"
assert (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree

get_metric(cm["fields"]["pets"], "tn") == 0
get_metric(cm["fields"]["pets"], "tn") == 0,
), "Expected 0 true negatives for pets field overall performance"

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to remove that extra comma and that should fix it

Comment on lines -365 to +346
get_metric(cm["fields"]["pets"]["fields"]["name"], "tp") == 1
), "Expected 1 true positives"
get_metric(cm["fields"]["pets"]["fields"]["name"], "tp") == 2
), "Expected 2 true positives"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, same comment as before https://github.com/awslabs/stickler/pull/72/changes#r2776109862
the cat buttons falls below the default match_threshold of the Pet object, therefore it's incorect to compare the values of the name. Even though they match.

Comment on lines -382 to +363
get_metric(cm["fields"]["pets"]["fields"]["species"], "tp") == 0
), "Expected 0 true positives"
get_metric(cm["fields"]["pets"]["fields"]["species"], "tp") == 1
), "Expected 1 true positives"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree

expected_top_aggregate = {
'tp': 12, 'fa': 1, 'fd': 4, 'fp': 5, 'tn': 0, 'fn': 1
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree

# Exact match: PROD-001 vs PROD-001 should be TP
assert field_metrics["overall"]["tp"] == 1, (
f"Nested field {field} should have 1 TP"
# Even though poor match, at the field level this is an exact match: PROD-002
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, this not right, per my comment below. https://github.com/awslabs/stickler/pull/72/changes#r2776109862

Copy link
Contributor

@sromoam sromoam left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we do need some changes to this PR.
Apologies @hayleypark that I didn't catch this earlier.

I recommend you strip the scope down to only the aggregate metrics and the tests here.

The changes to the non-aggregate tests are incorrect, after a review, becuase of the logic we have documented for how we evalaute list objects. https://github.com/awslabs/stickler/blob/d24894c86a2c52a62fa306748cbf2e7740faebe3/docs/docs/Core-Concepts/Threshold_Gated_Recursive_Evaluation.md
This is our current method, docuemnted, where we don't count any confusion matrix counts in objects that aren't matched up to the object's match_threshold, with the logic being, if you need this comparison ,you can turn that value down. IF you want to propose a change to this logic, that's fine, but we need to revise the docuemtnation as well.

Comment on lines 242 to 245
field_metrics["overall"]["tp"] + field_metrics["overall"]["fd"]
== 1
), f"Nested field {field} should have 1 comparison"
== 3
), f"Nested field {field} should have 3 comparisons"
elif field == "price":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, now that I'm understanding this better, this is not correct.
The reason is that the second and third products from the list do not match eachother respectively and therfore are considered false detections at the 'product' level. The way to make this assertion true in this test, would be to set the match_threshold on the Product object lower, so that the comparisons would proceed. The current match threshold at 0.8 precludes the 2nd and thrid list items, with the assertion that these are spuriously matched, and thereofre are not valid to compare to each-other.

# Exact match: PROD-001 vs PROD-001 should be TP
assert field_metrics["overall"]["tp"] == 1, (
f"Nested field {field} should have 1 TP"
# Even though poor match, at the field level this is an exact match: PROD-002
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, this not right, per my comment below. https://github.com/awslabs/stickler/pull/72/changes#r2776109862

Comment on lines -365 to +346
get_metric(cm["fields"]["pets"]["fields"]["name"], "tp") == 1
), "Expected 1 true positives"
get_metric(cm["fields"]["pets"]["fields"]["name"], "tp") == 2
), "Expected 2 true positives"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, same comment as before https://github.com/awslabs/stickler/pull/72/changes#r2776109862
the cat buttons falls below the default match_threshold of the Pet object, therefore it's incorect to compare the values of the name. Even though they match.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants