Skip to content

Collection Instance Filtering and SurrealDB Graph Traversal Improvements#650

Merged
lucksus merged 20 commits intodevfrom
feat/collection-isinstance-filter
Feb 13, 2026
Merged

Collection Instance Filtering and SurrealDB Graph Traversal Improvements#650
lucksus merged 20 commits intodevfrom
feat/collection-isinstance-filter

Conversation

@jhweir
Copy link
Contributor

@jhweir jhweir commented Jan 28, 2026

Collection Instance Filtering and SurrealDB Graph Traversal Improvements

Overview

This PR enhances the AD4M model system with custom graph traversal capabilities, improved timestamp tracking, and critical bug fixes for property assignment and collection filtering. The changes enable bi-directional relationship traversals while maintaining separation between Prolog and SurrealQL query paths.

Key Features

1. Custom Graph Traversal with getter (Commit: 1256af3)

Added getter option to @Property and @Collection decorators, enabling custom SurrealQL expressions for reverse link lookups and complex graph queries.

Key Changes:

  • Added getter field to PropertyMetadata and CollectionMetadata interfaces
  • Implemented evaluateCustomGettersForInstance() method to execute custom queries
  • Properties/collections with getter are skipped during normal link processing
  • SurrealQL expressions evaluated post-query using node graph context
  • Automatic filtering of 'None' and empty string values from results

Benefits:

  • Enables bi-directional relationship traversals (e.g., finding parent from child in reply chains)
  • Prevents Prolog/SurrealQL syntax mixing that causes engine crashes
  • Maintains clean separation between query engines

Example Usage:

@Optional({
  through: HAS_REPLY,
  getter: "(<-link[WHERE predicate = 'flux://has_reply'].in.uri)[0]"
})
replyingTo: string | undefined;

2. Enhanced Timestamp Tracking: createdAt and updatedAt (Commit: 1905ff1)

Changed from single timestamp tracking to dual timestamp system:

Changes:

  • createdAt: Earliest link timestamp (MIN) - when instance was first created
  • updatedAt: Most recent link timestamp (MAX) - when last modified
  • author: Changed from latest author to original author (based on createdAt)
  • timestamp: Now a backwards-compatible getter that returns createdAt

Implementation Details:

  • Both fields computed in-memory during getData() and instancesFromSurrealResult()
  • NOT persisted as @Property decorators - derived from link timestamps
  • SurrealDB queries updated to track both MIN and MAX timestamps
  • Prolog queries still return earliest timestamp (backwards compatible)

Benefits:

  • Proper temporal tracking of entity lifecycle
  • Maintains API compatibility with existing code
  • Enables audit trails and change history

3. SurrealDB Collection Filtering with condition

Added condition option to @Collection decorator's where clause, enabling SurrealDB-native graph traversal filtering for collection items.

Key Changes:

  • Added condition field to CollectionMetadata interface and WhereOptions
  • Updated SDNA generation to support collections with only condition (no Prolog conditions required)
  • Implemented filtering in Ad4mModel.evaluateCustomGettersForInstance() to apply conditions per-item
  • Updated PerspectiveProxy.getCollectionValuesViaSurreal() with same filtering logic
  • Added comprehensive test coverage in prolog-and-literals.test.ts

Benefits:

  • Enables complex graph traversal filters directly in SurrealDB (e.g., checking if item has specific relationship)
  • Avoids Prolog query limitations for advanced graph patterns
  • Maintains clean separation between Prolog instance checks and SurrealDB relationship filters
  • Improves query performance for relationship-based collection filtering

Example Usage:

@Collection({
  through: "recipe://entries",
  where: {
    condition: `WHERE in.uri = Target AND predicate = 'recipe://has_ingredient' AND out.uri = 'recipe://test'`
  }
})
ingredients: string[] = [];

Implementation Details:

  • Condition string supports variable substitution: $perspective, $base, and Target
  • If condition starts with WHERE, automatically wrapped in array::len(SELECT * FROM link ...) > 0 pattern
  • Filtering occurs post-query during instance hydration for optimal performance
  • Falls back gracefully if condition evaluation fails (keeps unfiltered values)

4. Documentation Improvements

SDNA Parallel Calls Limitation (Commit: e2c510b)

Added comprehensive documentation explaining why ensureSDNASubjectClass() cannot be called in parallel due to sdna_change_mutex in Rust executor.

Documentation Covers:

  • Root cause: Mutex serialization causing deadlocks with concurrent calls
  • Historical context: Limitation present since March 2024 (c6f1f96)
  • Current workaround: Sequential execution instead of Promise.all()
  • Four solution options with trade-offs analysis
  • Recommended hybrid approach with phased implementation
  • Migration guide for downstream consumers (Flux, we-*)

Model System Architecture Analysis (Commit: 3c14940)

Added three detailed technical documents:

  1. AD4M-MODEL-ARCHITECTURE-ANALYSIS.md: Complete assessment of the 3,320-line monolithic model architecture

    • Identifies God Object anti-pattern, dual query engine maintenance burden
    • Proposes phased refactor strategy (8-12 week timeline)
  2. CRDT-ORDERING-STRATEGY.md: Design proposal for abstracting P2P-safe ordering strategies

    • Introduces LinkedList, FractionalIndex, and Timestamp strategies
    • Automatic CRDT link generation for conflict-free list ordering
  3. DECORATOR-SYSTEM-REDESIGN.md: Comprehensive decorator API redesign

    • Eliminates semantic contradictions (@Optional with required: true)
    • Introduces relationship-based decorators (@HasMany, @BelongsTo)
    • Adds eager loading support and aligns with modern ORM patterns

Critical Bug Fixes

5. Property Assignment and Timestamp Handling (Commit: 0b5c094)

Fixed Issues:

  • "Cannot set property timestamp which has only a getter" error
  • ❌ CI test failures in Ad4mModel.test.ts

Solutions:

  • Filter read-only properties (getters without setters) before Object.assign()
  • Changed Prolog result mapping from "timestamp" to "createdAt" (Prolog returns earliest timestamp)
  • Updated SurrealDB attribute filtering to preserve "createdAt" and "updatedAt"
  • Maintained timestamp property as backwards-compatible getter

Technical Details:

  • Added filtering in assignValuesToInstance() to skip properties with getters but no setters
  • Updated instancesFromPrologResult() to map Timestamp → createdAt (line 1502)
  • Updated instancesFromSurrealResult() to preserve createdAt/updatedAt during attribute filtering (line 1754)

Tests Fixed:

  • ✅ "should filter properties when query specifies properties"
  • ✅ "should use Prolog when useSurrealDB is false in findAll()"
  • ✅ "should use Prolog when useSurrealDB(false) in ModelQueryBuilder.get()"

6. Collection Instance Filtering and Falsy Value Handling (Commit: 1389a97)

Fixed Issues:

  1. ❌ Getter-only property assignment errors
  2. where.isInstance filtering not applied to getter collections
  3. ❌ Valid falsy values (0, false) being dropped from collections
  4. ❌ Duplicate evaluateCustomGettersForInstance calls causing redundant queries

Solutions:

Fix 1: Explicitly distinguish accessor vs data descriptors

// Before: descriptor.writable was undefined for accessors, causing issues
// After: Check for get/set explicitly to identify accessors
if (descriptor.get && !descriptor.set) {
  // Skip getter-only properties
}

Fix 2: Apply isInstance filtering to getter collections

  • Collections with getter were previously skipped during filtering loop
  • Now properly instance-checked after evaluation in getData()

Fix 3: Preserve valid falsy values

// Before: if (value) { ... } // Drops 0, false, empty string
// After: if (value !== null && value !== undefined && value !== '' && value !== 'None')

Fix 4: Remove duplicate evaluation

  • Removed redundant evaluateCustomGettersForInstance call in findAll()
  • Consolidated to single evaluation pass
  • Prevents overwriting already-filtered collection values

7. Ad4m Connect Improvements

Z-Index and Mobile Detection (Commit: 66f0bec)

  • Moved z-index to positioned elements (.wrapper and .settings-button)
  • Changed settings button from absolute to fixed positioning with z-index
  • Added mobile detection (width < 800px) to ConnectionOptions component
  • Hide local node option on mobile devices
  • Added resize listener for responsive mobile detection

Invalid Token Handling (Commit: 158a6cd)

  • Removed early return in notifyAuthChange to ensure authstatechange event always fires
  • Automatically clear invalid tokens when InvalidSignature error is detected
  • Improved UX by preventing retry attempts with invalid credentials on page refresh
  • Reduces console error spam from expired/invalid tokens

Z-Index Increase (Commit: c1026de)

  • Increased z-index on ad4m-connect modal for better overlay behavior

Version Bumps

  • 0.11.2-dev.3 (Commit: 8d6403c) - After getter implementation
  • 0.11.2-dev.4 (Commit: 62a0fd5) - After timestamp tracking changes
  • 0.11.2-dev.5 (Commit: f375fea) - After collection filtering fixes
  • ad4m-connect 0.11.2-dev.5 (Commit: a5e6f41) - After auth improvements
  • 0.11.2-dev.6 - After condition implementation

Breaking Changes

Query Engine Property Naming Refactor

To establish SurrealDB as the default query engine and clarify which properties target which engine, decorator property names have been standardized:

Renamed to explicitly mark Prolog usage:

  • getterprologGetter (for Prolog-based property getters)
  • setterprologSetter (for Prolog-based property setters)
  • conditionprologCondition (for Prolog-based collection filtering)

Renamed to become the default (SurrealDB):

  • surrealGettergetter (SurrealDB is now the default for custom getters)
  • surrealConditioncondition (SurrealDB is now the default for collection filtering)

This makes SurrealDB features the intuitive default while explicitly marking legacy Prolog features.

Migration required: Update any decorators using the old getter, setter, or condition properties to use the prolog prefixed versions if they contain Prolog code.

Non-Breaking Changes

  • timestamp property remains as getter returning createdAt
  • author semantics changed but field name unchanged
  • Prolog queries continue to work unchanged

Migration Guide

For Developers Using timestamp:

// Old code continues to work:
const time = model.timestamp; // Returns createdAt

// New code can be more explicit:
const created = model.createdAt;
const modified = model.updatedAt;

For Developers Adding Custom Graph Traversals:

// Use getter for reverse lookups (SurrealDB - now the default):
@Optional({
  through: PARENT_LINK,
  getter: "(<-link[WHERE predicate = 'parent_link'].in.uri)[0]"
})
parent: string | undefined;

// Use condition for collection filtering (SurrealDB - now the default):
@Collection({
  through: "recipe://entries",
  where: {
    condition: `WHERE in.uri = Target AND predicate = 'has_tag' AND out.uri = 'tag://featured'`
  }
})
featuredEntries: string[] = [];

// If you need Prolog-specific code, use the prolog prefix:
@Optional({
  through: "recipe://rating",
  prologGetter: "triple(Base, 'recipe://user_rating', Rating), Value is Rating * 2"
})
rating: number = 0;

@Collection({
  through: "recipe://steps",
  where: {
    prologCondition: "triple(Target, 'step://order', Order), Order < 3"
  }
})
firstSteps: string[] = [];

For Downstream Consumers (Flux, we-*):

  • Review SDNA initialization patterns for parallel call issues
  • Consider switching to sequential ensureSDNASubjectClass() calls if experiencing hangs
  • Update any code relying on author field semantics (now returns original author, not latest)

Related Issues

  • Fixes hangs/timeouts with 12+ concurrent SDNA registrations
  • Resolves property assignment errors in model instantiation
  • Fixes collection instance filtering when using where.isInstance
  • Preserves falsy values (0, false) in collection results

Documentation

  • ✅ SDNA parallel calls limitation documented
  • ✅ Model architecture analysis completed
  • ✅ CRDT ordering strategy design documented
  • ✅ Decorator system redesign proposal created
  • ✅ Code examples added for getter usage

Future Work

Based on architectural analysis documents:

  1. Phased refactor of monolithic model system (8-12 weeks)
  2. Implementation of CRDT ordering strategies
  3. Decorator system redesign with relationship-based decorators
  4. Eager loading support
  5. Query builder improvements

Summary by CodeRabbit

Release Notes

  • New Features

    • Added support for custom SurrealQL getters on model properties and collections.
    • Improved timestamp tracking with separate creation and update timestamps (with backwards-compatible alias).
    • Added mobile-aware connection interface that adapts to smaller viewports.
  • Bug Fixes

    • Enhanced authentication error handling in embedded mode.
    • Improved modal visibility and settings button positioning.
  • Documentation

    • Added architectural analysis and strategic planning guides for model and decorator systems.

Implement filtering of collections by class instance type using the where.isInstance
option in @Collection decorators. This allows collections to automatically filter
items to only include instances of a specific class.

Changes:
- Add isInstance filtering in getData() for single instance loading
- Add isInstance filtering in instancesFromSurrealResult() for query results
- Support both class reference (isInstance: App) and string name (isInstance: 'Message') syntaxes
- Make PerspectiveProxy.getSubjectClassMetadataFromSDNA() and batchCheckSubjectInstances() public
- Use batch checking via batchCheckSubjectInstances() to efficiently filter collections

This enables cleaner collection definitions without custom Prolog conditions:
@Collection({ through: 'ad4m://has_child', where: { isInstance: App } })
…Ad4mModel

Add surrealGetter option to PropertyOptions and CollectionOptions decorators,
enabling custom SurrealQL expressions for reverse link lookups and complex
graph queries. This allows properties and collections to use graph traversal
syntax (e.g., <-link for incoming links) without breaking Prolog compatibility.

Key changes:
- Add surrealGetter field to PropertyMetadata and CollectionMetadata interfaces
- Implement evaluateSurrealGettersForInstance() method to execute custom queries
- Skip properties/collections with surrealGetter during normal link processing
- Evaluate surrealGetter expressions post-query using node graph context
- Filter out 'None' and empty string values from results
- Add comprehensive documentation for surrealGetter usage

This enables bi-directional relationship traversals (e.g., finding parent from
child in reply chains) while maintaining separation between Prolog and SurrealQL
query paths, preventing engine crashes from mixed syntax.

Example usage:
```typescript
@optional({
  through: HAS_REPLY,
  surrealGetter: "(<-link[WHERE predicate = 'flux://has_reply'].in.uri)[0]"
})
replyingTo: string | undefined;
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 28, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds optional surrealGetter metadata to decorators and model metadata; defers SurrealQL getter evaluation to a post-population evaluator; splits timestamp into createdAt/updatedAt (keeps timestamp alias); makes two PerspectiveProxy methods public; bumps package versions; adds multiple architecture and design docs; small connect UI/auth tweaks.

Changes

Cohort / File(s) Summary
Version bumps
core/package.json, connect/package.json
Incremented package versions in core and connect manifests.
Decorators metadata
core/src/model/decorators.ts
Added optional surrealGetter?: string to PropertyOptions and CollectionOptions.
Model core / Surreal getters
core/src/model/Ad4mModel.ts
Added surrealGetter to property/collection metadata; introduced createdAt/updatedAt and timestamp alias; added private static evaluateSurrealGettersForInstance(...); skip normal link resolution for surrealGetter fields and evaluate them after population; updated query/result mapping and author/timestamp derivation.
Perspective API visibility
core/src/perspectives/PerspectiveProxy.ts
getSubjectClassMetadataFromSDNA and batchCheckSubjectInstances changed from private to public (signatures unchanged).
Connect — embedded auth & UI
connect/src/core.ts, connect/src/web.ts, connect/src/components/views/ConnectionOptions.ts
Consolidated embedded-mode init/auth flow (explicit timeout, clear token on specific errors, always dispatch auth state); increased modal/settings z-index and made settings button fixed; ConnectionOptions hides Local Node UI on small viewports.
Design docs — SDNA parallel calls
docs/SDNA-PARALLEL-CALLS-ISSUE.md
New doc analyzing SDNA parallel-call deadlocks/timeouts and proposed mitigations (batch API, queueing, mutex tradeoffs).
Design docs — Model architecture
docs/AD4M-MODEL-ARCHITECTURE-ANALYSIS.md
New comprehensive architecture analysis and phased refactor plan for Ad4mModel/decorator system.
Design docs — CRDT ordering
docs/CRDT-ORDERING-STRATEGY.md
New proposal for CRDT-based ordering strategies, APIs, migration guidance, and strategy implementations.
Design docs — Decorator redesign
docs/DECORATOR-SYSTEM-REDESIGN.md
New proposal for unified Field/relationship decorator API, migration strategy, and codemod guidance.

Sequence Diagram

sequenceDiagram
    autonumber
    participant Client as Client
    participant Meta as ModelMetadata
    participant Loader as DataLoader
    participant Perspective as PerspectiveProxy
    participant Eval as SurrealQL_Evaluator
    participant Instance as ModelInstance

    Client->>Meta: getModelMetadata()
    Meta-->>Client: metadata (may include surrealGetter)
    Client->>Loader: request load instance(s)
    Loader->>Perspective: fetch SDNA / SurrealDB data
    Perspective-->>Loader: raw triples / links / timestamps
    Loader->>Loader: populate fields (skip surrealGetter fields)
    Loader->>Eval: evaluateSurrealGettersForInstance(instance, perspective, metadata)
    Eval->>Perspective: execute SurrealQL getters
    Perspective-->>Eval: getter results
    Eval->>Instance: assign evaluated property/collection values
    Instance-->>Client: fully populated instance(s) with createdAt/updatedAt
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • lucksus

Poem

🐰 I nibble metadata at dawn's first light,

getters hum and fetch the links just right.
Timestamps split to mark when moments start,
Surreal whispers stitch each scattered part.
A rabbit hops — new fields, a tiny delight!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The PR title focuses on 'Collection Instance Filtering and SurrealDB Graph Traversal Improvements,' which directly aligns with the surrealGetter feature (custom SurrealQL expressions for properties/collections) and timestamp/author improvements that enable bidirectional traversals across the codebase.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/collection-isinstance-filter

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
core/src/model/Ad4mModel.ts (1)

781-867: Use @ModelOptions className when resolving where.isInstance.

Line 841 uses the constructor .name, which can diverge from the SDNA class name (minified builds or custom @ModelOptions). This can lead to false negatives in where.isInstance filtering. Prefer the prototype/className fallback used elsewhere.

🛠️ Suggested fix
-              const className = typeof collMeta.where.isInstance === 'string' 
-                ? collMeta.where.isInstance 
-                : collMeta.where.isInstance.name;
+              const className = typeof collMeta.where.isInstance === 'string'
+                ? collMeta.where.isInstance
+                : (collMeta.where.isInstance.prototype?.className ||
+                   collMeta.where.isInstance.className ||
+                   collMeta.where.isInstance.name);
🤖 Fix all issues with AI agents
In `@core/src/model/Ad4mModel.ts`:
- Around line 1718-1756: The code currently calls
evaluateSurrealGettersForInstance twice for each instance which causes SurrealDB
re-queries and overwrites collections filtered by where.isInstance; keep a
single evaluation pass: run evaluateSurrealGettersForInstance once (so
collection values exist for filtering), perform the where.isInstance filtering
using perspective.getSubjectClassMetadataFromSDNA and
perspective.batchCheckSubjectInstances on metadata.collections entries, and
remove the second loop that re-invokes evaluateSurrealGettersForInstance to
avoid doubling queries and overwriting filtered instance[collName] values.

Add comprehensive documentation explaining why ensureSDNASubjectClass()
cannot be called in parallel due to sdna_change_mutex in Rust executor.

The document covers:
- Root cause: Mutex serialization causing deadlocks with concurrent calls
- Historical context: Limitation present since March 2024 (c6f1f96)
- Current workaround: Sequential execution instead of Promise.all()
- Four solution options with trade-offs analysis
- Recommended hybrid approach with phased implementation
- Migration guide for downstream consumers (Flux, we-*)

This addresses hangs/timeouts when 12+ SDNA registrations are made
concurrently, particularly during community/space initialization.
Changed timestamp tracking from single MAX to MIN/MAX (createdAt/updatedAt):
- createdAt: Earliest link timestamp (MIN) - when instance was first created
- updatedAt: Most recent link timestamp (MAX) - when last modified
- author: Changed from latest author to original author (based on createdAt)
- Added backwards-compatible timestamp getter that returns createdAt

Implementation details:
- Both fields computed in-memory during getData() and instancesFromSurrealResult()
- NOT persisted as @Property decorators - derived from link timestamps
- Updated SurrealDB queries to track both MIN and MAX timestamps
- Prolog queries still return earliest timestamp (backwards compatible)

This enables proper temporal tracking while maintaining API compatibility.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
core/src/model/Ad4mModel.ts (2)

786-845: createdAt/updatedAt ignore collection-only updates.
Line 802 updates timestamps only while iterating property links, so a collection update won’t advance updatedAt, and instances with only collection changes may never set createdAt. Consider computing min/max across all links once, then use that for author/timestamps.

🛠️ Proposed fix (compute timestamps once for all links)
-        let minTimestamp = null;
-        let maxTimestamp = null;
-        let latestAuthor = null;
-        let originalAuthor = null;
+        let minTimestamp = null;
+        let maxTimestamp = null;
+        let latestAuthor = null;
+        let originalAuthor = null;
+
+        // Track timestamps/authors across ALL links
+        for (const link of links) {
+          if (!link.timestamp) continue;
+          if (minTimestamp === null || link.timestamp < minTimestamp) {
+            minTimestamp = link.timestamp;
+            originalAuthor = link.author;
+          }
+          if (maxTimestamp === null || link.timestamp > maxTimestamp) {
+            maxTimestamp = link.timestamp;
+            latestAuthor = link.author;
+          }
+        }
...
-            // Track timestamps/authors for createdAt and updatedAt
-            if (link.timestamp) {
-              if (!minTimestamp || link.timestamp < minTimestamp) {
-                minTimestamp = link.timestamp;
-                originalAuthor = link.author;
-              }
-              if (!maxTimestamp || link.timestamp > maxTimestamp) {
-                maxTimestamp = link.timestamp;
-                latestAuthor = link.author;
-              }
-            }

1745-1753: Selective attribute filtering drops createdAt/updatedAt and breaks timestamp.
When properties/collections are specified, createdAt/updatedAt get deleted (Line 1750), which makes the timestamp getter return undefined. Consider keeping these fields alongside author.

🛠️ Proposed fix (preserve createdAt/updatedAt)
-            if (!requestedAttributes.includes(key) && key !== 'timestamp' && key !== 'author' && key !== 'baseExpression') {
+            if (
+              !requestedAttributes.includes(key) &&
+              key !== 'timestamp' &&
+              key !== 'author' &&
+              key !== 'baseExpression' &&
+              key !== 'createdAt' &&
+              key !== 'updatedAt'
+            ) {
               delete instance[key];
             }
🤖 Fix all issues with AI agents
In `@core/src/model/Ad4mModel.ts`:
- Around line 847-888: The loop in getData() skips collections with
collMeta.surrealGetter so their values (populated later by
ctor.evaluateSurrealGettersForInstance) never get the where.isInstance filter;
fix by applying the same isInstance filtering to surrealGetter collections after
evaluateSurrealGettersForInstance runs: after calling
ctor.evaluateSurrealGettersForInstance(this, this.#perspective, metadata)
iterate the metadata.collections entries where collMeta.surrealGetter is true,
read the populated values from (this as any)[collName], determine className the
same way (collMeta.where?.isInstance), call
this.#perspective.getSubjectClassMetadataFromSDNA and batchCheckSubjectInstances
as done for normal collections, and replace (this as any)[collName] with the
filtered result; preserve the existing try/catch behavior for errors.

…sals

Add three detailed technical documents analyzing and proposing improvements
to the AD4M model system:

- AD4M-MODEL-ARCHITECTURE-ANALYSIS.md: Complete assessment of the current
  3,320-line monolithic model architecture, identifying critical issues
  including God Object anti-pattern, dual query engine maintenance burden,
  contradictory decorator semantics, and excessive documentation. Proposes
  phased refactor strategy with estimated 8-12 week timeline.

- CRDT-ORDERING-STRATEGY.md: Design proposal for abstracting P2P-safe
  ordering strategies into @Collection decorator. Introduces LinkedList,
  FractionalIndex, and Timestamp strategies with automatic CRDT link
  generation for conflict-free list ordering in distributed environments.

- DECORATOR-SYSTEM-REDESIGN.md: Comprehensive redesign of the decorator
  API to eliminate semantic contradictions (@optional with required: true),
  introduce relationship-based decorators (@hasmany, @BelongsTo), add
  eager loading support, and align with modern ORM patterns while
  maintaining P2P/graph capabilities.

These documents serve as technical planning artifacts for future model
system improvements and refactoring efforts.
…mbedded auth handling

Core Changes (Ad4mModel):
- Fix "Cannot set property timestamp which has only a getter" error by filtering read-only properties before Object.assign()
- Change Prolog result mapping from "timestamp" to "createdAt" since Prolog returns earliest timestamp (creation time)
- Update SurrealDB attribute filtering to preserve "createdAt" and "updatedAt" instead of "timestamp" getter
- The "timestamp" property remains as a backwards-compatible getter that returns createdAt

Connect Changes (Ad4mConnect):
- Improve embedded mode auth state handling with clearer conditional logic
- Clean up comment formatting for timeout configuration

Technical Details:
- Add filtering in assignValuesToInstance() to skip properties with getters but no setters
- Update instancesFromPrologResult() to map Timestamp → createdAt (line 1502)
- Update instancesFromSurrealResult() to preserve createdAt/updatedAt during attribute filtering (line 1754)
- Maintain backwards compatibility via timestamp getter alias

Fixes CI test failures in Ad4mModel.test.ts:
- "should filter properties when query specifies properties"
- "should use Prolog when useSurrealDB is false in findAll()"
- "should use Prolog when useSurrealDB(false) in ModelQueryBuilder.get()"
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
connect/src/core.ts (1)

50-77: ⚠️ Potential issue | 🟠 Major

Prevent premature rejection while embedded auth check is still pending.

If AD4M_CONFIG arrives before connect() is called, this.ad4mClient can be set while checkAuth() is still running and authState is still the default "unauthenticated". The new branch rejects immediately and can fail even when auth later succeeds. Gate the rejection on an explicit “auth check finished” flag (or a transient auth state) to avoid this race.

🔧 Suggested fix (track auth completion)
@@
   authState: AuthStates = "unauthenticated";
+  private embeddedAuthChecked: boolean = false;
@@
   async checkAuth(): Promise<boolean> {
     try {
@@
       return true;
     } catch (error) {
@@
         return false;
       }
+    } finally {
+      this.embeddedAuthChecked = true;
     }
   }
@@
         if (this.ad4mClient) {
           if (this.authState === 'authenticated') {
             clearTimeout(timeout);
             console.log('[Ad4m Connect] Client already initialized in embedded mode');
             resolve(this.ad4mClient);
-          } else {
+          } else if (this.embeddedAuthChecked) {
             // Auth already failed before connect() was called
             clearTimeout(timeout);
             reject(new Error(`Embedded auth state: ${this.authState}`));
           }
         }
🤖 Fix all issues with AI agents
In `@core/src/model/Ad4mModel.ts`:
- Around line 761-770: The filter for writableProps allows accessor descriptors
without setters because descriptor.writable is undefined for accessors; change
the predicate in the Object.entries(propsObject).filter to explicitly detect
accessor descriptors via
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(instance), key) and only
allow accessors when descriptor.set is defined, otherwise for data descriptors
require descriptor.writable !== false (or no descriptor). Update the logic
around writableProps/Object.assign to use this explicit accessor vs data
descriptor check so getter-only properties are excluded before calling
Object.assign(instance, writableProps).
- Around line 970-975: The collection filtering uses a truthiness check that
incorrectly drops valid falsy values (e.g., 0, false); update the filter in
Ad4mModel (where you assign instance[collName] from result[0].value) to use the
same explicit checks as the property filter: replace the predicate "v && v !==
'' && v !== 'None'" with "v !== undefined && v !== null && v !== '' && v !==
'None'"; also make the identical change to the other collection filter instance
(the one around the second occurrence) so both collection filters match the
explicit null/undefined/empty/'None' checks used by the property handling.
🧹 Nitpick comments (1)
core/src/model/Ad4mModel.ts (1)

946-947: Use a word-boundary placeholder when substituting Base.

/Base/g can accidentally replace identifiers like Database or string literals. Replacing only whole-word Base avoids unintended SurrealQL mutations.

♻️ Suggested tweak
-          const query = (propMeta as any).surrealGetter.replace(/Base/g, safeBaseExpression);
+          const query = (propMeta as any).surrealGetter.replace(/\bBase\b/g, safeBaseExpression);
@@
-          const query = (collMeta as any).surrealGetter.replace(/Base/g, safeBaseExpression);
+          const query = (collMeta as any).surrealGetter.replace(/\bBase\b/g, safeBaseExpression);

Also applies to: 965-966

- Move z-index to positioned elements (.wrapper and .settings-button)
- Change settings button from absolute to fixed positioning with z-index
- Add mobile detection to ConnectionOptions component
- Hide local node option on mobile devices (width < 800px)
- Add resize listener for responsive mobile detection
- Remove early return in notifyAuthChange to ensure authstatechange event always fires
- Automatically clear invalid tokens when InvalidSignature error is detected
- Improve UX by preventing retry attempts with invalid credentials on page refresh
- Reduces console error spam from expired/invalid tokens
…nd falsy value handling

- Fix getter-only property assignment: Explicitly distinguish between accessor
  and data descriptors to prevent getter-only properties from being incorrectly
  assigned via Object.assign (descriptor.writable is undefined for accessors)

- Apply where.isInstance filtering to surrealGetter collections in getData():
  Collections with surrealGetter were previously skipped during the filtering
  loop and never got instance-checked after evaluation

- Fix collection filters to preserve valid falsy values: Replace truthiness
  checks with explicit null/undefined/empty/'None' checks to prevent dropping
  valid values like 0 or false

- Remove duplicate evaluateSurrealGettersForInstance in findAll(): Consolidate
  to single evaluation pass to avoid redundant SurrealDB queries and prevent
  overwriting filtered collection values
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
core/src/model/Ad4mModel.ts (1)

2350-2358: ⚠️ Potential issue | 🟠 Major

cleanCopy() does not exclude createdAt/updatedAt, leaking metadata into subject-class resolution.

createdAt and updatedAt are now own instance properties (class fields at lines 416-417), so Object.entries(this) will include them. The existing key !== "timestamp" guard is a dead branch since timestamp is a prototype getter.

This copy feeds into stringOrTemplateObjectToSubjectClassName() via innerUpdate() and get(). Including timestamp metadata in the class-name lookup may cause misidentification.

Proposed fix
 private cleanCopy() {
   const cleanCopy = {};
   const props = Object.entries(this);
   for (const [key, value] of props) {
-    if (value !== undefined && value !== null && key !== "author" && key !== "timestamp") {
+    if (value !== undefined && value !== null && key !== "author" && key !== "timestamp" && key !== "createdAt" && key !== "updatedAt") {
       cleanCopy[key] = value;
     }
   }
   return cleanCopy;
 }
🤖 Fix all issues with AI agents
In `@core/src/model/Ad4mModel.ts`:
- Around line 806-831: getData() updates minTimestamp/maxTimestamp and
originalAuthor/latestAuthor only while iterating properties, ignoring collection
links and causing different createdAt/updatedAt compared to
instancesFromSurrealResult(); modify getData() so timestamp/author tracking runs
over all links (both property and collection links) before computing
createdAt/updatedAt — either move the timestamp logic into a single preliminary
loop over links or add the same timestamp checks into the collection-processing
loop (the code around minTimestamp/maxTimestamp updates and the collection
handling block) so that timestamps are derived from every link just like
instancesFromSurrealResult().
- Around line 973-999: The current replacement of the placeholder in
surrealGetter uses a greedy regex (/Base/g) which will alter any identifier
containing "Base" (affecting code paths in Ad4mModel where surrealGetter is used
for metadata.properties and metadata.collections); update the replacement in
both loops that build `query` (the properties loop iterating over
`propName`/`propMeta` and the collections loop iterating over
`collName`/`collMeta`) to target only the standalone placeholder (e.g., use a
word-boundary regex like `\bBase\b` or an exact token replace) when substituting
`safeBaseExpression`, leaving other identifiers intact, and keep the existing
error handling and use of `perspective.querySurrealDB` unchanged.
- Around line 1808-1837: The code calls
perspective.getSubjectClassMetadataFromSDNA repeatedly inside the per-instance
loop causing redundant SDNA fetches; hoist and cache class metadata per target
class before iterating instances by scanning metadata.collections for
collMeta.where.isInstance, computing the targetClassName (same logic as now),
calling perspective.getSubjectClassMetadataFromSDNA once per unique
targetClassName, storing results in a map, and then inside the existing
instances loop use the cached classMetadata and call
perspective.batchCheckSubjectInstances(subjects, classMetadata); preserve
existing try/catch semantics and skip filtering when classMetadata is missing or
failed.

@jhweir jhweir changed the title isInstance Filtering and surrealGetter Support for Ad4mModel Collection Instance Filtering and SurrealDB Graph Traversal Improvements Feb 7, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@connect/src/web.ts`:
- Around line 93-99: The settings button CSS currently uses width: 34px; height:
34px; bottom: 10px; right: 10px which yields a touch target smaller than the
44×44px recommendation and may collide with OS safe areas; update the settings
button CSS block (the rule setting width/height/bottom/right) to preserve the
34px visual size but expand the interactive area to 44px by using padding or
min-width/min-height and box-sizing (e.g., add padding: 5px; box-sizing:
content-box or set min-width:44px; min-height:44px), and adjust the bottom/right
offsets to respect safe areas using env(safe-area-inset-bottom) /
env(safe-area-inset-right) (e.g., bottom: calc(10px +
env(safe-area-inset-bottom)); right: calc(10px + env(safe-area-inset-right)));
ensure pointer-events remain enabled for the larger hit area.
🧹 Nitpick comments (1)
connect/src/web.ts (1)

42-42: Consider using a CSS custom property for the z-index magic number.

99999 is repeated in both .wrapper and .settings-button. Extracting it into a CSS custom property (e.g., --ac-z-index: 99999) on :host would make it easier for consumers to override if it conflicts with their app's stacking context, and avoids the duplication.

Note that even inside Shadow DOM, position: fixed elements participate in the host document's stacking context, so a z-index this high could still clash with other overlays (e.g., third-party chat widgets, toast libraries). A configurable property would give integrators a relief valve.

Proposed refactor
   :host {
     --ac-primary-color: `#91e3fd`;
+    --ac-z-index: 99999;
     ...
   }

   .wrapper {
-    z-index: 99999;
+    z-index: var(--ac-z-index);
   }

   .settings-button {
-    z-index: 99999;
+    z-index: var(--ac-z-index);
   }

Also applies to: 97-99

Copy link
Member

@lucksus lucksus left a comment

Choose a reason for hiding this comment

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

Code looks fine, but there are not tests for surrealGetters, and the isInstance tests that I deactivated would need to be unskipped to show this new code is working.

…es + fix critical bugs

## New Tests
- Added 3 tests for surrealGetter feature:
  * Property evaluation with custom SurrealQL expressions
  * Collection evaluation with graph traversal queries
  * 'None'/empty value filtering

- Added 3 tests for isInstance filtering:
  * Collection filtering with class reference
  * Collection filtering with string class name
  * findAll() integration with isInstance constraints

## Critical Bug Fixes

### 1. Fixed GROUP BY bug in batchCheckSubjectInstances (PerspectiveProxy.ts)
- **Problem**: SurrealDB queries with `GROUP BY in.uri` only returned one result per expression set instead of all matching records
- **Impact**: isInstance filtering was only finding 1 of N valid instances
- **Solution**: Removed `GROUP BY in.uri` clause from both flag and property check queries
- **Evidence**: Diagnostic tests showed WITH GROUP BY = 1 result, WITHOUT = all results

### 2. Fixed @Flag property persistence in save() (Ad4mModel.ts)
- **Problem**: Flag properties and other metadata-defined initial values weren't being saved
- **Cause**: save() only iterated Object.entries(this), missing decorator-set prototype values
- **Solution**: Added metadata iteration to include properties with initial values from decorators
- **Impact**: Ensures @Flag properties are now properly persisted on first save

## Files Changed
- core/src/perspectives/PerspectiveProxy.ts: Removed GROUP BY from SurrealDB queries (lines 1503-1520)
- core/src/model/Ad4mModel.ts: Enhanced save() to include metadata initial values (lines 2320-2350)
- tests/js/tests/prolog-and-literals.test.ts: Added 6 comprehensive integration tests (~350 lines)

All tests passing: 85 passing, 6 pending, 0 failing ✅
lucksus
lucksus previously approved these changes Feb 12, 2026
Adds surrealCondition support to collection metadata/SDNA handling and applies SurrealDB-side filtering in Ad4mModel and PerspectiveProxy, with updated docs and tests for surrealCondition behavior.
lucksus and others added 2 commits February 12, 2026 20:28
Renamed model decorator properties to clarify query engine usage and
establish SurrealDB as the default/primary query system:

**Prolog-specific (explicitly prefixed):**
- `getter` → `prologGetter`
- `setter` → `prologSetter`
- `condition` → `prologCondition`

**SurrealDB (now default, no prefix):**
- `surrealGetter` → `getter`
- `surrealCondition` → `condition`

This naming convention makes SurrealDB the intuitive default while
explicitly marking legacy Prolog features, supporting the strategic
direction to deprecate Prolog in favor of SurrealDB's superior
performance (10-100x faster graph queries).

**Changed files:**
- core/src/model/Ad4mModel.ts: Updated interfaces, metadata, and evaluation methods
- core/src/model/decorators.ts: Updated PropertyOptions, CollectionOptions, SDNA generation
- core/src/perspectives/PerspectiveProxy.ts: Updated collection filtering and metadata types
- core/src/model/Ad4mModel.test.ts: Updated test decorators and assertions
- tests/js/tests/prolog-and-literals.test.ts: Updated 90+ test usages
- PR.md: Updated documentation and examples

All tests passing. No breaking changes to external APIs - decorators
maintain the same structure, just with clearer naming.
@lucksus lucksus merged commit 6800bbb into dev Feb 13, 2026
5 checks passed
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