Skip to content

feat(config): {ancestor:N} namespace placeholder for memory_dir rules (refs #296)#306

Merged
memtomem merged 1 commit intomainfrom
fix/auto-ns-ancestor-placeholder-296
Apr 19, 2026
Merged

feat(config): {ancestor:N} namespace placeholder for memory_dir rules (refs #296)#306
memtomem merged 1 commit intomainfrom
fix/auto-ns-ancestor-placeholder-296

Conversation

@memtomem
Copy link
Copy Markdown
Owner

Refs #296 (primitive; wizard preset direction-3 deferred, see below).

Problem

With enable_auto_ns=true applied to memory_dirs whose basename is
non-discriminating (memory, memories, plans — exactly the shape
produced by auto-discovered Claude/Codex provider dirs), the folder-based
fallback collapses every file to default. The guard in
_resolve_namespace suppresses the useless immediate-parent basename
but doesn't derive a useful alternative. The identifier users care
about lives one level up: FOO in ~/.claude/projects/FOO/memory/.

The issue author's realistic workaround — hand-crafting a
namespace.rules entry per project — is defeated by the fact that
NamespacePolicyRule.namespace only supports {parent}, which is
still the useless memory basename.

Fix (direction 2 of 3)

Add the {ancestor:N} placeholder to NamespacePolicyRule.namespace.
A rule like:

{
  "path_glob": "~/.claude/projects/*/memory/**",
  "namespace": "claude:{ancestor:1}"
}

expands to claude:FOO for that layout.

Semantics: {ancestor:N} resolves to file_path.parents[N].name.

N Folder
0 immediate parent (equivalent to {parent})
1 grandparent (the FOO-shaped project id for provider dirs)
2 great-grandparent
... ...

Validator extensions (all load-time rejections):

  • {ancestor} without an integer spec — caller must say which level.
  • Non-integer spec ({ancestor:abc}) — must parse as int.
  • Negative spec ({ancestor:-1}) — would silently wrap into filesystem
    root via parents[-1].
  • {parent:N} — format specs are ambiguous on the bare parent
    placeholder; force users onto {ancestor:N} for any positional lookup.

Runtime: _format_namespace is rewritten to walk
string.Formatter().parse() output, so {parent} and {ancestor:N}
can be mixed with literals in a single template. Out-of-range N or
empty folder names skip the rule and fall through (same shape as the
existing empty-{parent} skip), logged once per rule index.

What this PR explicitly does NOT do

The #296 issue author proposed (2) + (3):

  • (2) — this PR: primitive {ancestor:N} placeholder.
  • (3) — deferred: wizard preset. mm init's provider-dirs step
    should auto-append matching NamespacePolicyRule entries so the
    Claude/Codex memory dirs Just Work without users hand-crafting
    rules. Tracking remains on auto_ns silently collapses to default when memory_dir basename is non-discriminating (e.g., .../FOO/memory) #296; this PR closes the primitive gap
    but does not close the issue.
  • (1) — rejected: implicit fallback in _resolve_namespace for
    container-basename memory_dirs. The issue author explicitly didn't
    want this because it would silently change behavior for non-provider
    memory_dirs that legitimately rely on the current guard.

Sequencing (3) as a follow-up keeps the primitive reviewable on its
own (27-line validator + 34-line formatter rewrite + tests) and
separates the design questions in (3) (which provider dirs? what
namespace labels? migrating existing installs?) from the mechanical
change in (2).

Tests

7 new cases in test_indexing_engine.py:

Test Asserts
test_ancestor_placeholder_grandparent {ancestor:1} → grandparent folder name
test_ancestor_zero_equals_parent {ancestor:0}{parent}
test_ancestor_out_of_range_falls_through {ancestor:99} skips rule, logs once
test_ancestor_combined_with_literal {ancestor:1}/sub composes with literals
test_ancestor_without_index_rejected_at_load bare {ancestor} rejected
test_ancestor_non_integer_spec_rejected_at_load {ancestor:abc} rejected
test_ancestor_negative_index_rejected_at_load {ancestor:-1} rejected
test_parent_with_format_spec_rejected_at_load {parent:N} rejected, points to {ancestor:N}

Negative-tested: stashing the src changes → 7 ancestor tests fail
as expected. Existing {parent} tests remain green (no regressions in
the pre-existing behavior).

Scope boundaries

  • No change to _resolve_namespace's priority chain (explicit → rules
    enable_auto_ns → default). Only the rule-format step gains the
    new placeholder.
  • No wizard / config-migration changes.
  • No change to auto-discovery of memory_dirs.

Test plan

  • uv run ruff check && ruff format --check — clean.
  • uv run mypy packages/memtomem/src/memtomem/indexing packages/memtomem/src/memtomem/config.py — no issues.
  • uv run pytest packages/memtomem/tests/test_indexing_engine.py — 92 passed.
  • uv run pytest packages/memtomem/tests/ — 1863 passed (no regressions).
  • Negative-test: stash config.py + engine.py changes → all 7 new ancestor tests fail.
  • Manual: add a rule {"path_glob": "~/.claude/projects/*/memory/**", "namespace": "claude:{ancestor:1}"}, reindex a Claude project memory dir, confirm chunks land in claude:<project-id> namespace.

Related

🤖 Generated with Claude Code

… (refs #296)

When ``enable_auto_ns=true`` is applied to memory_dirs whose basename is
non-discriminating (``memory``, ``memories``, ``plans`` — exactly the
shape produced by auto-discovered Claude/Codex provider dirs), the
folder-based fallback collapses every file to ``default`` because its
guard suppresses the useless basename without deriving a useful
alternative. The parent one level further up (``FOO`` in
``~/.claude/projects/FOO/memory/``) is the identifier users actually
care about.

This commit ships the primitive from #296 direction (2): add the
``{ancestor:N}`` placeholder to ``NamespacePolicyRule.namespace`` so a
rule like

    { "path_glob": "~/.claude/projects/*/memory/**",
      "namespace": "claude:{ancestor:1}" }

expands to ``claude:FOO`` for that layout. Semantics: ``{ancestor:N}``
is ``file_path.parents[N].name``; ``N=0`` is the immediate parent
(equivalent to ``{parent}``), ``N=1`` is the grandparent, and so on.

Validator extensions:

- ``{ancestor}`` without an integer spec rejected at load time.
- Non-integer or negative spec rejected at load time — negatives would
  silently wrap into filesystem root via ``parents[-1]``.
- ``{parent:N}`` rejected too — format specs are ambiguous on the
  bare ``parent`` placeholder; force users onto ``{ancestor:N}`` for
  any positional lookup.

Runtime: ``_format_namespace`` rewritten to walk ``string.Formatter
.parse`` output, so ``{parent}`` and ``{ancestor:N}`` can be mixed
with literals in a single template. Out-of-range ``N`` or empty
folder names skip the rule and fall through (same shape as the
existing empty-``{parent}`` skip), logged once per rule index.

Follow-up (not in this PR): wizard preset — ``mm init``'s
provider-dirs step should append matching ``NamespacePolicyRule``
entries automatically so auto-discovered dirs Just Work without
users having to hand-craft rules. Tracked as direction (3) on #296.

Tests: 7 new cases covering happy path, ancestor-zero parity with
parent, out-of-range fall-through, literal composition, and four
load-time validation rejections. Negative-tested by stashing src
changes — all 7 fail without the primitive.

Co-Authored-By: Claude <[email protected]>
@memtomem memtomem merged commit 6b53cca into main Apr 19, 2026
7 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 19, 2026
@memtomem memtomem deleted the fix/auto-ns-ancestor-placeholder-296 branch April 19, 2026 23:08
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants