Skip to content

feat 185: add array_filter evaluator for compound platform predicates (#185)#425

Merged
IanMayo merged 6 commits intomainfrom
claude/implement-speckit-185-1AmhJ
Apr 14, 2026
Merged

feat 185: add array_filter evaluator for compound platform predicates (#185)#425
IanMayo merged 6 commits intomainfrom
claude/implement-speckit-185-1AmhJ

Conversation

@IanMayo
Copy link
Copy Markdown
Member

@IanMayo IanMayo commented Apr 13, 2026

Summary

  • Add array_filter() evaluator to the CQL2 filter engine for compound per-element predicates on platforms[] — eliminates false positives from cross-platform attribute mixing (e.g., "British submarines" no longer matches items where GB and subsurface are on different platforms)
  • Implement CQL2 JSON serialization and deserialization for array_filter expressions, enabling round-trip fidelity with the NL-to-CQL2 pipeline (feat: Add nested child selection support with path-based addressing #188)
  • Support taxonomy-expanded vessel_class matching and expression-level negation inside compound predicates
  • 32 new tests, 0 regressions across 1,273 existing tests, no new dependencies

Changes

Phase 2: Foundation — Type Definitions

  • Add PlatformField, CompoundPredicate (discriminated union), ArrayFilterPredicate types to types.ts
  • Extend FilterExpression with optional arrayFilters field (backward-compatible)
  • Export new types from index.ts

Phase 3: US1 — Compound Platform Filtering

  • Implement evaluateCompound() recursive predicate evaluator in matchers.ts
  • Implement matchArrayFilter() per-element matcher in matchers.ts
  • Extend matches() in engine.ts to evaluate arrayFilters as AND conditions
  • 12 tests: acceptance scenarios, OR sub-predicates, empty/null edge cases, mixed expressions

Phase 4: US2 — CQL2 JSON Serialization

  • Implement compoundPredicateToCql2() and arrayFilterToCql2() serialization in cql2-json.ts
  • Implement parseCql2Predicate() and cql2JsonToArrayFilters() deserialization
  • Extend filterExpressionToCql2Json() to include arrayFilters in output
  • 10 tests: serialize AND/OR, deserialize nested predicates, round-trip fidelity

Phase 5: US3 — Taxonomy Expansion

  • vessel_class comparisons inside compound predicates use existing DescendantMap for hierarchical matching
  • 4 tests: frigate→type23 expansion, nationality mismatch on same element, warship descendants, unknown node

Phase 6: US4 — Negation

  • negated flag on ArrayFilterPredicate inverts match (XOR with result)
  • NOT-wrapping in CQL2 JSON serialization/deserialization
  • 6 tests: evaluation negation, empty platforms, CQL2 NOT wrapping, round-trip

Evidence

Test Results

Metric Value
New Tests 32
Passed 32
Failed 0
Existing Tests 1,273 (unchanged)
Total Test Files 82

Key Scenarios Verified

  • Per-element compound matching: GB+subsurface compound filter rejects items where conditions are met by different platforms
  • CQL2 round-trip fidelity: serialize → deserialize → evaluate produces identical results
  • Taxonomy expansion inside compounds: "British frigates" matches Type 23, Type 26 through vessel_class hierarchy
  • Negation semantics: negated array_filter correctly inverts match
  • Edge cases: empty platforms, missing fields, case-insensitive comparison, case-sensitive ID matching

Usage Example

import { createFilterEngine } from '@debrief/components';
import type { FilterExpression } from '@debrief/components';

const engine = createFilterEngine({ taxonomy });

// Find items with a British submarine
const expression: FilterExpression = {
  predicates: [],
  orGroups: [],
  arrayFilters: [{
    array: "platforms",
    predicate: {
      kind: "and",
      children: [
        { kind: "comparison", field: "nationality", value: "GB" },
        { kind: "comparison", field: "domain", value: "subsurface" },
      ],
    },
  }],
};

// Item with GB surface + DE subsurface → NO match (cross-join eliminated)
// Item with GB subsurface → MATCH
const results = engine.filter(items, expression);

Test Plan

  • All 3 spec acceptance scenarios for compound matching (mixed, single, two-element)
  • OR sub-predicates within compound expressions
  • Empty platforms array → false, null/missing fields → false
  • Multiple arrayFilters AND'd together
  • Mixed expressions (existing predicates + arrayFilters)
  • Case-insensitive matching (non-id fields) and case-sensitive (id field)
  • Taxonomy expansion: frigate → type23/type26, warship → all descendants
  • Negation: excludes matching items, includes non-matching, empty → true
  • CQL2 JSON serialization: AND, OR, mixed, single-child reduction, NOT wrapping
  • CQL2 JSON deserialization: array_filter, nested AND/OR, NOT-wrapped, embedded in AND root
  • Round-trip: serialize → deserialize → evaluate produces identical results
  • Backward compatibility: all 1,273 existing tests pass unchanged

Related

https://claude.ai/code/session_012oktLgzBZa6Vun213kBdjp

claude added 6 commits April 13, 2026 22:03
Add PlatformField, CompoundPredicate, and ArrayFilterPredicate types
for compound per-element filtering on STAC item platform arrays.
Extend FilterExpression with optional arrayFilters field.

https://claude.ai/code/session_012oktLgzBZa6Vun213kBdjp
Add matchArrayFilter() and evaluateCompound() for per-element compound
predicate matching on platforms[]. Supports AND/OR logic, case-insensitive
comparison, taxonomy expansion for vessel_class, and negation.
Extends engine.matches() to evaluate arrayFilters as AND conditions.

19 new tests covering US1, US3, US4 acceptance scenarios plus edge cases.

https://claude.ai/code/session_012oktLgzBZa6Vun213kBdjp
Implement serialize/deserialize for array_filter expressions:
- compoundPredicateToCql2() and arrayFilterToCql2() for serialization
- parseCql2Predicate() and cql2JsonToArrayFilters() for deserialization
- NOT-wrapping for negated array_filter expressions
- Round-trip fidelity: serialize→deserialize→evaluate produces identical results

13 new tests covering serialization, deserialization, round-trips, and negation.

https://claude.ai/code/session_012oktLgzBZa6Vun213kBdjp
- Remove unused ArrayFilterPredicate imports from test files
- Replace index-signature platform type with PlatformRecord + explicit
  field accessor for strict type compatibility
- Define Cql2Node interface for CQL2 JSON deserialization to reduce
  unsafe Record<string, unknown> casts

https://claude.ai/code/session_012oktLgzBZa6Vun213kBdjp
- Evidence: test-summary.md (32/32 pass), usage-example.md, code-example-output.txt
- Media: shipped-post.md, linkedin-shipped.md
- Mark all tasks complete in tasks.md
- Mark backlog item 185 as complete

https://claude.ai/code/session_012oktLgzBZa6Vun213kBdjp
@IanMayo IanMayo temporarily deployed to debrief-preview-pr-425 April 13, 2026 22:16 Inactive
@github-actions
Copy link
Copy Markdown

🚀 Preview Deployments

Code Server (Full VS Code Extension)

🖥️ Open Code Server

Browser-based VS Code with the Debrief extension and sample data pre-installed.

Web Shell (Standalone App)

📱 Open Web Shell

Use this for Playwright testing and demos - runs outside Storybook.

Storybook (Component Library)

📚 Open Storybook

Browse all components, stories, and documentation.


All Links
Environment Code Server Web Shell Storybook
This PR Open IDE Open App Open Storybook
Main branch Open App Open Storybook

Updated on commit 6495656

@IanMayo IanMayo changed the title feat(filter-engine): add array_filter evaluator for compound platform predicates (#185) feat 185: add array_filter evaluator for compound platform predicates (#185) Apr 13, 2026
@IanMayo IanMayo merged commit 322929e into main Apr 14, 2026
8 checks passed
@IanMayo IanMayo deleted the claude/implement-speckit-185-1AmhJ branch April 14, 2026 04:56
github-actions bot pushed a commit that referenced this pull request Apr 14, 2026
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.

2 participants