Skip to content

symfonic.memory.scope_isolation

scope_isolation

Prefix-isolation primitives shared across all memory backends (v8.0 §5).

The isolation rule (design §5.d): a query at path P sees a memory at path Q iff Q is a prefix of P. This is enforced via exact-set-membership over the query's ancestor-prefix strings — NEVER a LIKE 'prefix%' scan, which would leak acme-corp into an acme query (the §5.b R2 CRITICAL class).

Filtering is ALWAYS-ON (it closes the Item D cross-session bleed bug) and is NOT gated behind scorer_on_hot_path. Weighted blending (§4) is the opt-in part; isolation is unconditional.

The materialised scope_path is stored in the zero-migration property bag (MemoryNode.properties['scope_path'] / vector metadata scope_path) so no schema change is required. Pre-v8.0 rows have no scope_path; they dual-read as a 1-level root path derived from tenant_id (design §6.b), keeping them tenant-global as before.

is_visible

is_visible(
    stored_path: str, query_prefixes: list[str]
) -> bool

Return True iff stored_path is one of the query's ancestor prefixes.

Exact set membership — the §5.d isolation invariant. query_prefixes is :meth:TenantScope.ancestor_prefix_paths. NEVER a startswith.

Source code in src/symfonic/memory/scope_isolation.py
def is_visible(
    stored_path: str,
    query_prefixes: list[str],
) -> bool:
    """Return True iff ``stored_path`` is one of the query's ancestor prefixes.

    Exact set membership — the §5.d isolation invariant.  ``query_prefixes`` is
    :meth:`TenantScope.ancestor_prefix_paths`.  NEVER a ``startswith``.
    """
    return stored_path in query_prefixes

materialise_scope_path

materialise_scope_path(scope: TenantScope) -> str

Return the materialised scope_path string for a write scope.

Source code in src/symfonic/memory/scope_isolation.py
def materialise_scope_path(scope: TenantScope) -> str:
    """Return the materialised ``scope_path`` string for a write scope."""
    return scope.scope_path

metadata_scope_path

metadata_scope_path(
    metadata: dict[str, object] | None, tenant_id: str
) -> str

Vector-backend variant of :func:stored_scope_path (metadata bag).

Source code in src/symfonic/memory/scope_isolation.py
def metadata_scope_path(
    metadata: dict[str, object] | None,
    tenant_id: str,
) -> str:
    """Vector-backend variant of :func:`stored_scope_path` (metadata bag)."""
    return stored_scope_path(metadata, tenant_id)

mongo_prefix_filter

mongo_prefix_filter(
    scope: TenantScope, field: str = SCOPE_PATH_KEY
) -> dict

Build the Mongo prefix-isolation filter for a scope_path field.

Returns a query fragment matching docs whose field is one of the query's ancestor prefixes (exact $in — NEVER a $regex prefix, which would re-introduce the false-prefix-leak class). Includes the §6.b dual-read for pre-v8.0 docs missing the field: they behave as a 1-level root path. Compose into a larger find filter with $or-free AND by merging into the top-level dict, or under $and when other keys overlap.

Source code in src/symfonic/memory/scope_isolation.py
def mongo_prefix_filter(scope: TenantScope, field: str = SCOPE_PATH_KEY) -> dict:
    """Build the Mongo prefix-isolation filter for a scope_path field.

    Returns a query fragment matching docs whose ``field`` is one of the
    query's ancestor prefixes (exact ``$in`` — NEVER a ``$regex`` prefix,
    which would re-introduce the false-prefix-leak class).  Includes the §6.b
    dual-read for pre-v8.0 docs missing the field: they behave as a 1-level
    root path.  Compose into a larger ``find`` filter with ``$or``-free AND by
    merging into the top-level dict, or under ``$and`` when other keys overlap.
    """
    prefixes = scope.ancestor_prefix_paths()
    legacy = legacy_scope_path(scope.tenant_id)
    cond: dict = {field: {"$in": prefixes}}
    if legacy in prefixes:
        # Legacy (missing/absent) docs read as the root path; include them only
        # when the root path is on the query's ancestor chain.
        return {"$or": [cond, {field: {"$exists": False}}, {field: None}]}
    return cond

sql_prefix_condition

sql_prefix_condition(
    scope: TenantScope, column: str, next_param_index: int
) -> tuple[str, list[object]]

Build the SQL prefix-isolation predicate for a materialised column.

Returns (sql_fragment, params) where the fragment is an exact-IN over the query's ancestor prefixes (NEVER LIKE — the §5.b R2 guard):

(col = ANY($N::text[]) OR (col IS NULL AND $M = ANY($N::text[])))

The first branch matches v8.0 rows; the second is the §6.b dual-read for pre-v8.0 rows whose scope_path is NULL — they behave as a 1-level root path tenant\x1f<tenant_id>, visible to any descendant query under that tenant. next_param_index is the 1-based positional placeholder to use first (asyncpg $N style); the function consumes two placeholders.

Source code in src/symfonic/memory/scope_isolation.py
def sql_prefix_condition(
    scope: TenantScope,
    column: str,
    next_param_index: int,
) -> tuple[str, list[object]]:
    """Build the SQL prefix-isolation predicate for a materialised column.

    Returns ``(sql_fragment, params)`` where the fragment is an exact-IN over
    the query's ancestor prefixes (NEVER ``LIKE`` — the §5.b R2 guard):

        (col = ANY($N::text[]) OR (col IS NULL AND $M = ANY($N::text[])))

    The first branch matches v8.0 rows; the second is the §6.b dual-read for
    pre-v8.0 rows whose ``scope_path`` is NULL — they behave as a 1-level root
    path ``tenant\\x1f<tenant_id>``, visible to any descendant query under that
    tenant.  ``next_param_index`` is the 1-based positional placeholder to use
    first (asyncpg ``$N`` style); the function consumes two placeholders.
    """
    prefixes = scope.ancestor_prefix_paths()
    legacy = legacy_scope_path(scope.tenant_id)
    arr_idx = next_param_index
    legacy_idx = next_param_index + 1
    fragment = (
        f"({column} = ANY(${arr_idx}::text[]) "
        f"OR ({column} IS NULL AND ${legacy_idx} = ANY(${arr_idx}::text[])))"
    )
    return fragment, [prefixes, legacy]

stored_scope_path

stored_scope_path(
    properties: dict[str, object] | None, tenant_id: str
) -> str

Resolve a stored row's effective scope_path (with dual-read).

Reads properties['scope_path'] when present. Pre-v8.0 rows (no key) fall back to the 1-level root path tenant\x1f<tenant_id> (design §6.b) — visible to every descendant query under that tenant, the correct conservative default.

Source code in src/symfonic/memory/scope_isolation.py
def stored_scope_path(
    properties: dict[str, object] | None,
    tenant_id: str,
) -> str:
    """Resolve a stored row's effective ``scope_path`` (with dual-read).

    Reads ``properties['scope_path']`` when present.  Pre-v8.0 rows (no key)
    fall back to the 1-level root path ``tenant\\x1f<tenant_id>`` (design §6.b)
    — visible to every descendant query under that tenant, the correct
    conservative default.
    """
    raw = (properties or {}).get(SCOPE_PATH_KEY)
    if isinstance(raw, str) and raw:
        return raw
    return legacy_scope_path(tenant_id)