Skip to content

fix: avoid silently remapping explicit domain identities through substring collisions #721

Open

Description

@cdeust

Problem

In the real isolated ingestion campaign, remember(domain="scifact-comparison") persisted all 1812 facts under harness-comparison. Corpus verification initially found no rows in the requested domain. The resolver scans known repository fragments and can remap a supplied domain because a fragment matches another repository name.

Proposed solution

Distinguish explicit domain identity from an inferred path/name hint. Preserve the caller-selected identity or reject ambiguous resolution explicitly; preserve documented aliases deliberately rather than through accidental substring collisions.

Acceptance evidence

  • Reproduce the collision with a controlled repository registry and the public remember/recall contract.
  • An explicit scientific corpus domain cannot silently become an unrelated comparison repository domain.
  • Changing an unrelated repository in the registry cannot change the identity of an already explicit domain.
  • Document migration/alias behavior and ensure write/search/fetch agree on canonical scope.

Alternatives considered

Keep the current behavior and document its limits. This remains valid where the added guarantee has no user need; the issue should close with a documented product decision rather than assume that copying Hindsight is always appropriate. Reuse existing Cortex mechanisms before adding a second pipeline.

Scope assessment

MEDIUM. Raised from the October 10 Hindsight/Cortex comparison. Static observations refer to the pinned revisions below. They do not establish successful execution of Hindsight's whole pipeline or a repository-wide absence in Cortex. Priority is a proposal, not a release blocker.

Evidence

mcp_server/shared/domain_mapping.py:398; mcp_server/handlers/remember.py:67; observed before any scored recall, with exact document/text pairs verified after adapting to the persisted domain.

Activity

  1. cdeust commented on Nov 5, 2026

    @cdeust
    OwnerAuthor

    CI autopilot : tranche 1 livrée dans #877 (Refs #721, ne ferme pas l'issue).

    • Cause racine reproduite : resolve_domain traite le domaine comme un indice et renvoie le canonique du plus long fragment de dépôt contenu dans l'entrée ; avec un dépôt harness-comparison, remember(domain="scifact-comparison") écrivait harness-comparison (test du handler en échec sans le correctif, vert avec).
    • Correctif : resolve_explicit_domain — seul un nom de dépôt exact (ou sa famille ADR-0647) est mappé ; tout autre nom est conservé tel quel, en minuscules.

    Tranches restantes :

    1. Priorité directory > domain dans remember alors que le schéma décrit domain comme un override — décision du propriétaire.
    2. recall/fetch ne résolvent pas domain : écriture et recherche ne s'accordent pas sur le scope canonique (ex. cortex-cowork → cortex à l'écriture seulement).
    3. Documenter alias et migration des lignes déjà écrites sous un domaine collisionné.

    Generated by Claude Code

  2. cdeust commented on Nov 6, 2026

    @cdeust
    OwnerAuthor

    CI autopilot : tranche 2 livrée dans #901 (Refs #721, ne ferme pas l'issue).

    • Cause racine : remember résout un domain explicite par resolve_explicit_domain (fix(remember): keep an explicit domain instead of remapping it by substring (#721, slice 1) #877), mais recall passait l'argument brut au store. recall(domain="cortex-cowork") cherchait donc dans un scope où aucune écriture n'avait eu lieu (cortex), et recall(domain="SciFact") ne trouvait pas scifact.
    • Correctif : recall résout le domaine de la même façon ; un domaine vide reste non scopé. 7 tests sur le handler public (6 échouent sans le correctif).
    • Non vérifié : tests PostgreSQL, gate de mutation, benchmarks (torch non téléchargeable dans cette session).

    Tranches restantes :

    1. Priorité directory > domain dans remember, alors que le schéma décrit domain comme un override : décision du propriétaire.
    2. recall_hierarchical et les autres filtres par domaine ne résolvent pas encore l'argument.
    3. Documenter les alias et la migration des lignes déjà écrites sous un domaine collisionné.

    Generated by Claude Code

  3. cdeust commented on Nov 6, 2026

    @cdeust
    OwnerAuthor

    CI autopilot : tranche 3 livrée dans #914 (Refs #721, ne ferme pas l'issue ; commit b803e88).

    • Cause racine : remember et recall résolvent un domain explicite par resolve_explicit_domain, mais recall_hierarchical passait l'argument brut à get_memories_for_domain : recall_hierarchical(domain="cortex-cowork") construisait la hiérarchie sur un scope où aucune écriture n'a lieu.
    • Correctif : le handler public résout le domaine dans _scoped_by_explicit_domain avant _handler_impl ; un domaine blanc compte comme absence de domaine (ValidationError ADR-0045 R3). 8 tests, 7 échouent sans le correctif.
    • Non vérifié : tests PostgreSQL, gate de mutation, benchmarks (torch non téléchargeable dans cette session).

    Tranches restantes :

    1. Priorité directory > domain dans remember, alors que le schéma décrit domain comme un override : décision du propriétaire.
    2. Les autres lecteurs prenant un domain (unified_search, drill_down, narrative…) : vérifier pour chacun si c'est un scope mémoire ou un domaine de profil, puis résoudre.
    3. Documenter les alias et la migration des lignes déjà écrites sous un domaine collisionné.

    C'est la 3e tentative sur cette issue : le prochain passage posera autopilot:hold si une tranche restante ne peut pas être fusionnée.


    Generated by Claude Code

  4. cdeust commented on Nov 6, 2026

    @cdeust
    OwnerAuthor

    CI autopilot : tranche 4 dans #915 (Refs #721, ne ferme pas l'issue ; commit 538082e).

    • Cause racine : narrative passait le domain brut à get_memories_for_domain, alors que remember écrit sous resolve_explicit_domain(domain) ; narrative(domain="cortex-cowork") lisait un scope sans écriture.
    • Correctif : résolution par resolve_explicit_domain dans le handler ; domaine blanc = pas de domaine. 7 tests, 6 échouent sans le correctif.
    • Non vérifié : suite PostgreSQL, gate de mutation, benchmarks (torch non téléchargeable dans cette session).

    Les tranches précédentes ont été fusionnées (#914 entre autres), le plafond de 3 tentatives sans fusion ne s'applique donc pas.

    Tranches restantes :

    1. Priorité directory > domain dans remember alors que le schéma décrit domain comme un override : décision du propriétaire.
    2. Autres lecteurs qui passent un domain brut à get_memories_for_domain : get_project_story, drill_down, validate_memory, assess_coverage, detect_gaps, query_methodology (pour chacun, vérifier qu'il s'agit d'un scope mémoire et pas d'un domaine de profil).
    3. Documenter les alias et la migration des lignes déjà écrites sous un domaine collisionné.

    Generated by Claude Code

  5. 4 remaining items

  6. cdeust commented on Nov 7, 2026

    @cdeust
    OwnerAuthor

    CI autopilot : tranche 7 dans #928 (Refs #721, ne ferme pas l'issue ; commit fd16b57).

    • Cause racine : assess_coverage passait le domain brut à get_memories_for_domain, alors que remember écrit sous resolve_explicit_domain(domain) ; assess_coverage(domain="cortex-cowork") notait un scope sans écriture.
    • Correctif : résolution par resolve_explicit_domain dans le handler ; domaine blanc = lecture globale ; le domain renvoyé est le scope lu. 7 tests, 6 échouent sans le correctif ; tests_py/handlers : 1909 passed, 81 skipped.
    • Non vérifié : suite PostgreSQL, suite complète, gate de mutation, benchmarks (torch non téléchargeable dans cette session).

    Les tranches 1 à 6 ont été fusionnées : le plafond de 3 tentatives sans fusion ne s'applique pas.

    Tranches restantes :

    1. Priorité directory > domain dans remember : décision du propriétaire.
    2. validate_memory, detect_gaps, query_methodology passent encore un domain brut à get_memories_for_domain (vérifier pour chacun qu'il s'agit d'un scope mémoire et pas d'un domaine de profil).
    3. Documenter les alias et la migration des lignes déjà écrites sous un domaine collisionné.

    Generated by Claude Code

  7. cdeust commented on Nov 7, 2026

    @cdeust
    OwnerAuthor

    CI autopilot : tranche 8 dans #929 (Refs #721, ne ferme pas l'issue ; commit eae007a).

    • Cause racine : validate_memory passait le domain brut à get_memories_for_domain, alors que remember écrit sous resolve_explicit_domain(domain). validate_memory(domain="cortex-cowork") validait un scope sans écriture.
    • Correctif : le handler résout le domaine par resolve_explicit_domain, et un domaine blanc compte comme une lecture globale. 7 tests, dont 6 échouent sans le correctif. tests_py/handlers : 1915 passed, 81 skipped, 1 failed. Cet échec (sqlite-vec absent du venv local) se produit aussi sur main.
    • Non vérifié : suite PostgreSQL, suite complète, gate de mutation, benchmarks (torch n'est pas téléchargeable dans cette session).

    Les tranches 1 à 7 ont été fusionnées : le plafond de 3 tentatives sans fusion ne s'applique pas.

    Tranches restantes :

    1. Priorité directory > domain dans remember : décision du propriétaire.
    2. detect_gaps et query_methodology passent encore un domain brut à get_memories_for_domain. Il faut vérifier pour chacun qu'il s'agit d'un scope mémoire et pas d'un domaine de profil.
    3. Documenter les alias et la migration des lignes déjà écrites sous un domaine collisionné.

    Generated by Claude Code

  8. cdeust commented on Nov 7, 2026

    @cdeust
    OwnerAuthor

    CI autopilot : tranche 9 dans #930 (Refs #721, ne ferme pas l'issue ; commit 411c855).

    • Cause racine : query_methodology lisait les mémoires chaudes sous l'id de profil cognitif de detect_domain, alors que remember sans domaine explicite écrit sous resolve_cwd(cwd) (puis le profil résolu comme indice hors dépôt connu). Un id de profil différent de ce scope listait un scope sans écriture.
    • Correctif : lecture sous le scope que remember écrirait pour le même cwd. 6 tests, dont 5 échouent sans le correctif. tests_py/handlers : 1922 passed, 81 skipped.
    • detect_gaps vérifié : il ne passe que des domaines relus dans le store, aucun changement nécessaire (tranche restante n°2 close).
    • Non vérifié : suite PostgreSQL, suite complète, gate de mutation, benchmarks (torch non téléchargeable dans cette session).

    Les tranches 1 à 8 ont été fusionnées : le plafond de 3 tentatives sans fusion ne s'applique pas.

    Tranches restantes :

    1. Priorité directory > domain dans remember (et le repli par indice resolve_domain, qui garde la collision par sous-chaîne hors dépôt connu) : décision du propriétaire.
    2. Documenter les alias et la migration des lignes déjà écrites sous un domaine collisionné.

    Generated by Claude Code

  9. added
    autopilot:holdCI autopilot stood down: a human decision is required
    on Nov 7, 2026
  10. cdeust commented on Nov 7, 2026

    @cdeust
    OwnerAuthor

    CI autopilot: on hold, owner decision needed. Slices 1 to 9 are merged (the last one is #930, 8e47b0b). Both remaining slices depend on a product choice, so I am not opening a PR on a guess.

    Question to decide (mcp_server/handlers/remember.py:74, _resolve_domain): today, when a call passes both directory and domain, resolve_cwd(directory) wins and the explicit domain is ignored. That contradicts the acceptance criterion "an explicit domain cannot silently become another domain". There are three options:

    • (a) explicit domain > directory: the caller's identity always wins, and directory is only a hint;
    • (b) directory > domain, kept and documented as intentional;
    • (c) reject the call (explicit error) when both are given and resolve to two different scopes.

    The same choice applies to the hint fallback resolve_hint(detected) (when resolve_cwd resolves nothing): it can still remap through a substring match outside a known repository.

    What follows from the decision: slice 2 (documenting aliases, and migrating rows already written under a collided domain) depends on it. With (a) or (c), existing rows need either a migration or a documented reassignment. With (b), the documentation alone is enough.

    A comment from cdeust on this issue makes it eligible again.


    Generated by Claude Code

  11. cdeust commented on Nov 12, 2026

    @cdeust
    OwnerAuthor

    Décision : cette issue n'attend plus de réponse du propriétaire.

    Le propriétaire a demandé le 12 novembre 2026 que les issues avancent sans lui ; il ne répondra pas aux questions posées ici. Ce commentaire de cdeust lève le hold et précise la demande. Il vaut pour toute la suite de cette issue.

    • Tranche toi-même chaque question restée ouverte : d'abord une décision écrite et acceptée du dépôt (ADR accepté, docs/), puis ce que le texte de l'issue propose ou exclut, puis les standards du dépôt ; à défaut, la plus petite tranche qui réalise ce que l'issue demande. Une valeur qui manque se mesure, elle ne s'invente pas.
    • Écris la décision dans un commentaire marqué <!-- ci-autopilot decision --> (ce qui est retenu, la raison et sa source, ce qui est écarté, comment revenir en arrière), puis livre la tranche dans une PR. Une décision écrite ne se rejoue pas aux passages suivants.
    • Ne repose pas autopilot:hold pour une question : seulement pour l'irréversible, un accès que tu n'as pas, une dépendance nommée encore ouverte ou le plafond de tentatives, en nommant exactement ce qui manque.
    • Jamais de tag, de release, de montée de version du projet ni de déclenchement manuel d'un workflow.

    Règle complète : #343 (comment)

    Écrit par la session « écosystème autonome » sur instruction du propriétaire (12 novembre 2026).


    Generated by Claude Code

  12. removed
    autopilot:holdCI autopilot stood down: a human decision is required
    on Nov 12, 2026
  13. cdeust commented on Nov 12, 2026

    @cdeust
    OwnerAuthor

    Hold autopilot:hold levé : un commentaire de cdeust (« Décision : cette issue n'attend plus de réponse du propriétaire », 12/11/2026) a été posté après la pose du hold.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions