# ADR-0225 `directory` residue-sweep allowlist.
# Started by C10 (0047 §5) as a classification; promoted by C15 into the
# allowlist of `scripts/check_directory_residue.py`, the lint C10 wrote it for.
#
# WHAT THIS IS
# ------------
# ADR-0225 retires `directory` as a scoping/identity concept and says the
# retirement is "enforced by a residue sweep rather than by review".
# `scripts/check_directory_residue.py` is that sweep and this file is its
# allowlist: every scanned `*.py` that still carries a residue token in a
# SCOPING POSITION (parameter name / call-site keyword / string-literal dict
# key or subscript / class-body field), on purpose, with the reason recorded.
#
# Read the lint's module docstring first. In particular: `directory` was NOT
# removed. C5 removed its ability to RESOLVE; C14 measured that it survives as
# a parameter on 46 MCP tools in two classes, one of which is STILL
# directory-keyed. Deleting entries tagged `tool-surface` on the assumption the
# sweep is unfinished will break live scoping.
#
# FORMAT
# ------
#   <tag>  <path-or-glob>  # reason (>= 40 chars, required)
#
# A glob ends in `/**` and matches every scanned `*.py` beneath it. Paths are
# repo-relative. Malformed rows, unknown tags, duplicate paths, and reasons
# shorter than 40 chars are HARD FAILURES — an unparsed row silently grants
# nothing and hides a residue site. 40 matches the repo's governed-allowlist
# family (.test-weakening-, .health-endpoint-, .urllib-httperror-close-) and
# C9a's own reason floor; the lint promoting three siblings must not be the
# weakest of the four on the one field a reviewer actually reads.
#
# TAGS — the class of reason. The trailing comment gives the specific one.
# -----------------------------------------------------------------------
#   carve-out-1   Host-side identity MINTING, and only there: the hook entry
#                 points and the CLI/installer surface that hands a real path
#                 in so a `project_id` can be derived from it. Permanent.
#   carve-out-2   A SIGNATURE named after the stored legacy column
#                 (`directory_context`). The column itself is stripped as a
#                 CLASS and never appears here (see below). Dies with the
#                 column, in the NEXT PR's drop migration.
#   carve-out-3   A genuine filesystem path: opened, globbed, walked, watched,
#                 hashed, or turned into display text. Never compared against a
#                 project identity. Permanent.
#   tool-surface  The `directory` parameter C14 measured as SURVIVING on the 46
#                 MCP tools. Two sub-classes, named per entry:
#                   * resolve_effective_project — RAISES without `project=`;
#                     `directory` is accepted-but-inert.
#                   * accept_project_param      — STILL directory-keyed;
#                     `project=` is validated only. Removing `directory` here
#                     breaks scoping today.
#   wire-contract A key crossing a process boundary — queue payload, viz node,
#                 admin payload, `RecallRequest`. Renamed with the far side or
#                 not at all (`extra="forbid"`; images deploy together).
#   legacy-key    A REAL scope key that plan 0047 §2 Amendment 3 blocks: a
#                 rename is safe only when the backing table already carries
#                 `project_id` AND every caller actually holds an identity.
#                 C11 shipped the columns; these are the transitional second
#                 arm that keeps the un-backfilled historical corpus reachable,
#                 plus the sites whose callers still cannot produce an identity.
#
# CARVE-OUT 2 IS A CLASS STRIP, NOT ENTRIES
# -----------------------------------------
# `directory_context` in a STRING-LITERAL position (`row["directory_context"]`,
# `{"directory_context": ...}`) is the stored column, alive until the next PR's
# drop migration because the C6 backfill derives from it. The lint strips it
# before matching, so it never needs an entry — enumerating ~100 column reads
# would produce an allowlist that asserts nothing. A PARAMETER or KEYWORD of
# that name is a signature, not a column, and does get an entry.
#
# CARVE-OUT 4 IS MOOT HERE, BY CONSTRUCTION
# -----------------------------------------
# `docs/**/adr-*`, `docs/plans/archive/**` and `docs/CHANGELOG.md` are historical
# and correct as written. An AST lint over `*.py` never reaches markdown, so
# there is nothing to exempt. Recorded rather than omitted, because carve-out 4
# is on the reviewer's list. C14 owns the docs surface.
#
# DIRECTION 2 IS A HARD FAIL, NOT A WARNING
# -----------------------------------------
# An entry whose path matches no scanned file, or whose files no longer carry
# any residue, FAILS the lint. Deliberately unlike
# `.test-weakening-allowlist.json`'s `_stale_policy`: that guard's input is a
# merge-base that MOVES, so its entries go stale through nobody's fault. This
# lint's input is the filesystem — a stale entry is always somebody's edit and
# always has an owner. C15a also demonstrated the alternative: two warn-only
# entries sat stale from the ADR-0215 train's merge until a car finally deleted
# them, which is warn-only teaching the next reader to skim the warnings.
#
# KNOWN GAP — THE GLOBS (ADR-0080: state what the gate cannot see)
# ----------------------------------------------------------------
# The three `/**` entries below cover 37 `.py` files as a block. A NEW scoping
# `directory` inside e.g. `yadgar/core/install/` therefore passes silently.
# They are kept because they are genuine carve-out-3 subtrees and enumerating
# them file-by-file risks red-on-arrival churn — but this is a gap, not
# coverage. Narrow a glob to exact paths when a subtree stops being uniformly
# carve-out-3.
#
# C10 CLASSIFIED 25 FILES THAT NEED NO ENTRY HERE
# -----------------------------------------------
# C10's original list came from a word-boundary TEXT scan and included files
# whose only `directory` is a positional argument, a local variable, an
# attribute read (`args.directory`), or prose. The AST matcher excludes every
# one of those positions by construction, so those entries would be stale on
# arrival and were dropped rather than carried. Their C11/C9c worklist role is
# recorded in `docs/CHANGELOG.md` and plan 0047, which is where planning state
# belongs — not in a lint allowlist that Direction 2 must keep honest.

# ── carve-out 1: host-side identity minting ─────────────────────────────────
carve-out-1  yadgar/core/hooks/session-start-context.py    # the minting hook: turns the session's real cwd into a project_id
carve-out-1  yadgar/core/hooks/post-tool-capture.py        # hook entry point; stamps the host path onto the action_log payload

# ── carve-out 3: genuine filesystem paths, permanent ────────────────────────
carve-out-3  yadgar/core/install/**                        # install targets, config + interpreter paths (glob; see KNOWN GAP)
carve-out-3  yadgar/core/daemon/**                         # unit files and data dirs; watch_directory is a path to watch
carve-out-3  yadgar/core/staleness/**                      # file-hash staleness inputs — real files on disk
carve-out-3  yadgar/core/bootstrap/bootstrap.py            # watch_directory forwarded to the daemon watcher; a path, not a scope
carve-out-3  yadgar/core/server/_startup.py                # watch_directory forwarded into core_init_engines
carve-out-3  yadgar/core/seed/_scan.py                     # Path(directory).resolve() — a genuine filesystem walk
carve-out-3  yadgar/core/code_graph/config.py              # the repo path code_graph indexes; resolver keys the knob off it
carve-out-3  yadgar/_shared/runtime/lifecycle.py           # init_engines(watch_directory) — the directory the watcher watches
carve-out-3  yadgar/_shared/runtime/cache_epoch.py         # hashes its argument into a counter FILE NAME; producer is _resolve_project_root
carve-out-3  yadgar/_shared/server_helpers/server_helpers.py  # _resolve_project_root and the two worktree-root heuristics: paths on disk
carve-out-3  yadgar/_shared/file_queue/queue.py            # _find_terminal walks the queue directory looking for a terminal file
carve-out-3  yadgar/backend/retrieval/core.py              # basename(directory) is EMBEDDING INPUT TEXT, not a scope key; changing it
                                                           # silently shifts new rows out of the existing corpus's cosine space.
carve-out-3  yadgar/backend/admin_exec/adr_seed.py         # basename() project-NAME surrogate for display; never compared to an identity

# ── carve-out 2: a signature named after the stored directory_context column ─
carve-out-2  yadgar/_shared/storage/_project_id_writer.py  # _resolve_project_id_for_write(directory_context) — the column's own writer
carve-out-2  yadgar/_shared/storage/user.py                # insert_profile / get_profiles_for_entity take the column value
carve-out-2  yadgar/_shared/storage/narrative.py           # get_beliefs_for_subject(directory_context) + the NarrativeEntry field
carve-out-2  yadgar/_shared/wiki/store.py                  # the §25 read ladder: _gate_dir_eligible, find_similar_wiki_pages, read_by_directory
carve-out-2  yadgar/_shared/contracts/models.py            # pydantic mirrors of stored rows (Episode, Checkpoint, NarrativeEntry, prospective)
carve-out-2  yadgar/backend/write_exec/wiki_add_impl.py    # forwards directory_context= into the wiki write path
carve-out-2  yadgar/backend/queue_drainer/dlq.py           # forwards directory_context= when re-applying a drained payload

# ── wire contracts: keys crossing a process boundary ────────────────────────
wire-contract  yadgar/backend/embed_service/embed_service_models.py  # RecallRequest.directory retained OPTIONAL and deliberately not deleted;
                                                                     # extra="forbid", so core and backend images must move together.
wire-contract  yadgar/_shared/wiki/contract.py             # WikiAddOptions.directory_context — the enqueue-time payload field
wire-contract  yadgar/backend/graph/graph_nodes.py         # viz node payload key consumed by the graph UI
wire-contract  yadgar/backend/write_exec/_memorize_phases/_phase_post_write.py  # the same viz node payload key, written here
wire-contract  yadgar/backend/queue_drainer/__init__.py    # queue payload key — caller_context, read back by the drainer
wire-contract  yadgar/backend/admin_exec/wiki.py           # core-owned admin payload key over the HTTP admin boundary
wire-contract  yadgar/backend/admin_exec/seed.py           # core-owned admin payload key; directory and project_id coexist by design

# ── legacy-key: a real scope key, blocked by Amendment 3's two conditions ────
legacy-key  yadgar/_shared/storage/blocks.py               # memory_block: C11 declared project_id, the directory arm keeps pre-C11 rows reachable
legacy-key  yadgar/_shared/storage/runtime_config.py       # runtime_config: C11 shipped the COLUMN only; no caller holds an identity (plan §6 knob train)
legacy-key  yadgar/_shared/storage/queue.py                # action_log: dual-write; get_unprocessed_actions has exactly ONE non-test consumer, backend/consolidation/cleanup.py:192
legacy-key  yadgar/_shared/storage/episode.py              # episode: dual-write; backend/consolidation/cls.py:208 (_process_new_episodes) filters on ep['directory'] (causal_discovery/pc.py:60 already filters project_id)
legacy-key  yadgar/_shared/storage/ops.py                  # get_active_checkpoint — the checkpoint table's transitional dual key
legacy-key  yadgar/_shared/storage/memory.py               # get_recent_memories_since: both callers would have to resolve, and resolve raises;
                                                           # project_brief has no error envelope and runs on the session-start hook path.
legacy-key  yadgar/_shared/storage/wiki.py                 # list_wiki_pages / list_wiki_catalog + the three upserts that ALREADY have project_id
                                                           # (a two-parameter merge is a redesign, not a rename).
legacy-key  yadgar/_shared/sensory_buffer/sensory_buffer.py  # episode capture dual-writes the legacy column
legacy-key  yadgar/_shared/rules_engine/rules_engine.py    # _LEGACY_SCOPE_KINDS maps the RETIRED kind "directory" onto "project" so writes reject it
legacy-key  yadgar/backend/restoration/checkpoint_restore.py  # 9 residue sites (not five); checkpoint + memory_block DO carry project_id (migration 033, _C11_PROJECT_ID_TABLES), but callers can still pass project_id=None (arm 2: no identity to resolve)
legacy-key  yadgar/backend/restoration/__init__.py         # run_restore forwards into checkpoint_restore.restore
legacy-key  yadgar/backend/write_exec/checkpoint_impl.py   # forwards into replay.create_checkpoint — the checkpoint table again
legacy-key  yadgar/backend/write_exec/action_log_impl.py   # storage.insert_action_log(directory=) — the action_log dual-write, same block as _shared/storage/queue.py
legacy-key  yadgar/backend/consolidation/cleanup.py        # reads the action_log row's directory as the summary memory's scope
legacy-key  yadgar/backend/prospective/prospective.py      # prospective_memory.target_directory — a stored column with no project_id
legacy-key  yadgar/backend/admin_exec/blocks.py            # storage block API (directory=) — memory_block, same block as _shared/storage/blocks.py
legacy-key  yadgar/backend/admin_exec/project.py           # storage.get_block / create_block(directory=)
legacy-key  yadgar/backend/admin_exec/audit.py             # storage.insert_action_log(directory=) plus the payload key it reads back
legacy-key  yadgar/backend/admin_exec/runtime_config.py    # plan §6 — runtime_config scoping belongs to the knob train, not a mechanical rename
legacy-key  yadgar/core/server/http.py                     # MIXED Starlette hook routes: checkpoint, memory_block, runtime_config, action_log
legacy-key  yadgar/core/forward.py                         # _forward_restore — checkpoint DOES carry project_id (migration 033), but http.py:884 and cli/restore.py can still pass project_id=None (arm 2: no identity to resolve)
legacy-key  yadgar/core/runtime_config_client.py           # the host-side runtime_config client; blocked with its storage layer
legacy-key  yadgar/core/code_graph/digest.py               # memory_block injection scope is an exact directory match (the block NAME carries the discriminator)
legacy-key  yadgar/core/cli/_shared.py                     # forward_restore / forward_pre_compact_drain — checkpoint
legacy-key  yadgar/core/cli/capture.py                     # action_log: the CLI capture path stamps the legacy column
legacy-key  yadgar/core/cli/hook.py                        # memory_block plus the mixed hook routes; blocked with core/server/http.py
legacy-key  yadgar/core/cli/seed.py                        # memory + wiki_page writes still keyed on the legacy column
legacy-key  yadgar/core/cli/stats.py                       # memory rows are still counted by the legacy directory column
legacy-key  yadgar/core/seed/_generate.py                  # stamps the legacy column onto every seeded memory payload
legacy-key  yadgar/core/server/tools/_runtime_config.py    # the core-side runtime_config resolver; blocked with its storage layer

# ── tool-surface: the `directory` parameter C14 measured as SURVIVING ───────
# resolve_effective_project sub-class — RAISES without `project=`; `directory`
# is accepted-but-inert. accept_project_param sub-class — STILL directory-keyed.
tool-surface  yadgar/core/server/tools/_project_param.py   # THE two-class site itself: resolve_effective_project and accept_project_param
tool-surface  yadgar/core/server/tools/recall.py           # resolve_effective_project class; also _resolve_shape_limit's runtime_config read
tool-surface  yadgar/core/server/tools/memorize.py         # resolve_effective_project class — raises without project=; directory is accepted-but-inert
tool-surface  yadgar/core/server/tools/wiki.py             # resolve_effective_project class (wiki_add/adr) + wiki_list, which is still keyed
tool-surface  yadgar/core/server/tools/adr.py              # resolve_effective_project class over wiki_page
tool-surface  yadgar/core/server/tools/adr_render.py       # resolve_effective_project class over wiki_page
tool-surface  yadgar/core/server/tools/agent_prompts.py    # resolve_effective_project class over wiki_page
tool-surface  yadgar/core/server/tools/dispatch_helper.py  # resolve_effective_project class over memory + wiki_page
tool-surface  yadgar/core/server/tools/misc.py             # checkpoint (misc.py:137) is accept_project_param and STILL directory-keyed; restore (misc.py:216-234) was promoted to resolve_effective_project
tool-surface  yadgar/core/server/tools/project.py          # project_brief is accept_project_param and STILL directory-keyed
tool-surface  yadgar/core/server/tools/blocks.py           # block_* are accept_project_param and STILL directory-keyed (memory_block)
tool-surface  yadgar/core/server/tools/audit.py            # audit_anchors is accept_project_param and STILL directory-keyed
tool-surface  yadgar/core/server/tools/admin_other.py      # get_rules and recent_memories are accept_project_param and STILL directory-keyed
tool-surface  yadgar/core/server/tools/runtime_config.py   # config_* are accept_project_param; runtime_config is plan §6's knob train
tool-surface  yadgar/core/server/tools/task.py             # forwards directory= into the still-keyed admin payloads
tool-surface  benchmarks/run_eval.py                       # drives recall() over the tool surface that still accepts directory
tool-surface  benchmarks/run_longmemeval.py                # drives recall() over the tool surface that still accepts directory
tool-surface  benchmarks/run_perf_loadtest.py              # recall load test driving the same still-accepting tool surface as the two eval harnesses
carve-out-2  yadgar/core/migrations/rekey_corpus.py      # Car D (2026-08-14): the migration script that reads directory_context off the corpus and stamps project_id; dies with the column in the next PR's drop migration
wire-contract  yadgar/_shared/runtime/session_map.py  # the directory->project_id translation table itself: `directory` is the LOOKUP KEY, which is the module's whole reason to exist. C5 removed `directory`'s ability to RESOLVE (derive a key from a path) and that stays removed — nothing here derives. The SessionStart hook mints host-side via _identity_mint, where the working tree exists, and registers the pair; the daemon only asks "has the hook told me about this exact path?". An unregistered directory returns None and the caller fails loud, exactly as before this tier existed, so no key is ever manufactured from a path. Needed because a client with ONE global `mcpServers` entry makes every MCP request identical on the wire, and the daemon runs stateless_http (core/server/_startup.py) so there is no Mcp-Session-Id either — `directory` is the only per-call signal that varies. Per-project `mcpServers` users take the X-Yadgar-Project-Id header path instead and never reach this module. Dies when the wire carries a real per-session identity.
