# Scope target/access legacy debt inventory

> Canonical path: `guardrails/message/scope-target-access-debt-inventory.md`
> Guardrail: [`scope-target-access-contract.sh`](./scope-target-access-contract.sh)
> Spec: [`specs/message/scope-target-access-contract.md`](../../specs/message/scope-target-access-contract.md)

This inventory classifies remaining target/access-adjacent legacy naming and
resolver recovery debt after the scope target/access contract became the shared
message-domain boundary. It is intentionally durable: reviewers should update it
when a compatibility surface disappears, when a worker slice burns down one
category, or when a guardrail check moves a category from inventory-only to
enforcing.

## Space→scope semantic legacy boundaries

Space→scope legacy is not just `*Id`. The old `space` mental model can survive
through type names, helper/file names, resolver shims, double-read fallbacks,
JSON fields, SQL aliases, fixtures, error text, and active specs even after a
field is no longer named `spaceId`. Current message-domain code should use
`scope`, `scopeID`, or a role-specific name such as `deliveryScopeID`,
`permissionCheckScopeID`, `targetRef`, `readScope`, or `parentScope`.

The words `spaceID`, `SpaceID`, `spaceId`, `SpaceKind`, `SourceSpace*`,
`DeliverySpaceID`, `PermissionCheckSpaceID`, `SpaceProjection*`,
`SpaceParticipant*`, `space_projection.go`, `space_*` helpers, and
space-named resolver fallbacks are not product concepts for current
message-domain runtime code. The global policy is delete-first: after the
space→scope migration, legacy aliases, fallback chains, and old semantic names
must be removed unless a current hard constraint is proven. Clients and schema have moved to scope terminology, so legacy names should be deleted rather than
wrapped whenever the owning surface is touched. Retaining a legacy name requires
the owner/reviewer/PR to call out the exact reason, boundary, and exit condition.

### Allowed semantic legacy exceptions and follow-ups

These categories are the only allowed current exceptions. Each item has a
reason, boundary, and exit condition so the inventory is a ratchet, not a parking
lot for new compatibility.

| Category | Reason | Boundary | Exit condition |
| --- | --- | --- | --- |
| **Historical migrations and rollback paths** | Versioned migrations need to name old inputs or down-migration outputs. | `internal/tidb/migrations/**`, historical migration tests, and rollback SQL only. | New forward schema/bootstrap/admin/current DTOs use scope names; old migration names disappear only if migration retention policy allows deletion. |
| **True workspace concepts** | Workspace is a product/tenant concept, not the old message scope concept. | `workspace*` / `Workspace*` names that refer to tenants, memberships, machines, or workspace routing. | None; do not classify workspace as scope debt. |
| **True channel route/read surfaces** | Some public routes and response fields are channel-scoped by product API shape. | Route parameters such as `/api/messages/channel/:channelId`, message response `channelId`, task review decision bridges, and explicit channel notification APIs. | Rename only through a product/API migration; do not use these route fields as generic message-create aliases. |
| **Private-agent conversation writer boundary** | The local writer still owns durable-author vs routing/notification-principal separation, owner-private routing, freshness/idempotent replay, attachment claim echo, reply-thread participant/subscription mutation, active-task projection, and scope fanout-outbox plan construction. | `internal/store/privateagent/conversation_write.go` and conversation helper names only; it must write scope data and must not be named as a space writer. | Delete the privateagent-local writer after those responsibilities move into an equivalent shared message-domain write service with tests and guardrails. |
| **Daemon history/search scoped query contract** | The compatibility alias has been removed; this row documents the now-enforced boundary. | Agent internal history/search scoped cursor query parsing and focused tests. `channel` remains a human target ref only for history/search route parsing, not a scope-id fallback. | Keep `scope_id`-only; any future external constraint must add a documented adapter row before reintroducing compatibility. |
| **Task collaborator scope-edge schema** | The task collaborator People grant edge is owner authorization metadata, not a separate participant-space scope. | Current bootstrap/admin/runtime carry no legacy edge column; grants use `task_scope_participant_metadata.owner_scope_execution_id`. Old `participant_space_id` names are allowed only in historical migrations, test fixtures for old bootstrap, and 00146 rollback/down SQL. | Remove old migration/testdata mentions only if migration retention policy allows deleting those historical inputs; do not reintroduce the term in runtime, current schema, specs, or new tests. |
| **Provider-watch/private-agent scope-membership wording** | The wording has been corrected to owner-private scope visibility / scope membership projection; this row documents the now-enforced boundary. | Provider-watch/private-agent tests and comments may mention scope membership/projection only. | Keep ratchet preventing `space participant` wording from returning. |
| **Explicit invalid legacy request-key tests** | Negative coverage must prove retired request keys fail closed or are ignored when canonical keys are present without making those retired names look like current product concepts. | Focused daemon scoped-cursor, attachment upload, and message-create rejection tests may construct retired request keys dynamically behind helpers. Runtime code, DTOs, helpers, comments, and live inventory output must not carry those names as ordinary fields. | Remove the helper boundary when the rejection tests are no longer needed; until then, keep direct literals and `Space*` test names out of current test code. |
| **Legacy table names inside guardrails/spec history** | Cutover guardrails need to mention old table names to prevent regressions. | Guardrail patterns, baselines, and active specs that explicitly describe deprecated storage names such as `space_messages` or `space_participants`. | Remove when the corresponding guardrail category is retired; never copy those names back into runtime code. |
| **Explicit external hard constraints** | A third-party or operator-facing contract may require a legacy term temporarily. | The owner must document the contract next to the boundary and translate to typed scope roles before business logic. | Remove the adapter and this row entry when the external constraint is gone. |

Compatibility adapters require explicit exception evidence. A fallback from
`space*` to `scope*` is no longer allowed by default. Any remaining adapter must
name the hard external constraint, live in this inventory, and have a
guardrail/test preventing the fallback from spreading.

### Canonical Agent Runtime resolve-channel is scope-only

No resolve-channel daemon alias remains. The resolve-channel scope-only
contract is the current default and has no legacy response fallback.

`/api/v1/workspaces/:workspaceId/agents/:agentId/runtime/resolve-channel` emits
only `scopeId` plus the canonical `targetKey`; desktop
chat/review/file-target clients decode only `scopeId` and fail closed on stale
`spaceId`-only responses. This inventory no longer treats a legacy
resolve-channel `spaceId` response as an acceptable steady-state boundary.

### Agent target resolver scope-named wrappers

Agent-facing app-store resolver wrappers no longer preserve legacy channel/space
mental-model names. `internal/store/app/agent_messages.go`, trigger owner ports,
and testkit adapters expose `ResolveWritableAgentTargetScopeID`,
`ResolveReadableAgentTargetScopeID`, and `ResolveAgentReviewSubjectScopeID` when
they return scope ids for daemon targets. The old
`ResolveChannelIDForAgentTarget`, `ResolveReadableChannelIDForAgentTarget`, and
`ResolveReviewSubjectIDForAgentTarget` compatibility wrappers have been deleted
from this boundary. Lower-level helpers may keep channel-id names only when they
actually return app-compatible channel ids for true channel route/read surfaces;
they must not be reused as generic agent scope resolver wrappers.

### No daemon receive/history/search `space_id` query alias

Server-side agent `/receive` has been removed. The canonical workspace-scoped
Agent Runtime `/history` and `/search` endpoints accept the `scope_id` query
parameter for scoped cursors. The old `space_id` query alias has been removed;
`space_id`-only requests fail closed and do not enter scoped store reads. When
callers send both `scope_id` and `space_id`, the canonical `scope_id` value is
the only value used. Focused negative tests construct retired query keys
dynamically behind explicit helpers so the old field name does not reappear as
live test-helper or inventory wording. `channel` remains only as a user-facing
target reference for history/search boundary resolution and is not a scope-id
alias.

### Attachment upload scopeId-only boundary

Browser `/api/attachments/upload` and the canonical workspace-scoped Agent
Runtime `/upload` endpoint accept `scopeId` only. The old attachment upload
`spaceId` multipart alias and attachment-store `ResolvePostTargetForUser`
fallback have been removed; callers must name the upload scope explicitly
before entering attachment storage. Focused negative tests may construct
retired upload keys dynamically behind explicit helpers, but live upload code
and ordinary test-helper names must stay scope-named. The durable
`attachments.scope_id` column is already canonical, so no attachment DB
compatibility alias remains in the live upload/read/claim path.

### Browser/public message create scopeId-only boundary

Browser and public message creation now accepts only `scopeId` for
`POST /api/messages` and `POST /api/messages/batch`. The old create-message
`spaceId` request alias and the `channelId` request-body fallback have been
removed; route-level `channelId` remains valid only for routes whose path is
explicitly channel-scoped, such as message history reads. The web client API is
`writeMessageToScope` / `writeScopeMessagesBatch`, and direct API/e2e callers
must send `scopeId` rather than preserving generic space terminology. Daemon e2e
message-create payloads are part of this boundary and must not carry retired
`spaceId` request keys outside explicit negative-test helpers.

### Task parity and metadata-send scope naming

Task parity message announcement and adjacent metadata-send paths must use
scope/target terminology. The task announcement request and web type use
`targetScopeId`, not `targetChannelId`. `SendMessageForUserWithMetadata` ports
that accept a concrete scope use `scopeID`; ports that accept a human-readable or
trigger target string use `targetRef`. True channel route/read/review decision
fields may keep `channelID`, but they are not a fallback for generic message
create or task announcement targets.

### Task collaborator access helper scope naming

Task collaborator access helpers that check target membership use scope naming:
`ensureMemberCanReadScope(scopeID)` authorizes against `scope_memberships` for
channel/thread task-collaborator targets. True `workspaceID`, `ChannelID`, and
`threadChannelID` values remain product/route concepts, but the membership helper
must not return to `ensureMemberInSpace` or generic `spaceID` / `SpaceID` /
`spaceId` locals.

### Message history/search/read model scope naming

Message history/search/read-model records use scope naming internally:
`ThreadMessageRecord.ScopeID`, `ScopeKind`, and `ParentScopeID`; search store
parameters use `scopeID`; visible-thread/unread query aliases use
`parent_scope_id` and `scope_id` when they describe scope facts. True
`channel_id` route/read response fields remain channel-shaped, but read-model
internals must not reintroduce `SpaceID`, `SpaceKind`, `ParentSpaceID`,
`parent_space_id`, or generic `space_id` aliases for scope facts.

### Private-agent/app projection and owner quote burn-down

Private-agent/app projection helpers and owner-quote source records no longer
use generic space terminology as their internal contract. The canonical files are
`internal/store/privateagent/scope_projection.go` and
`internal/store/app/scope_projection.go`; organization bridging depends on a
`ScopeProjectionStore`; owner-private quote records use `SourceScopeID`; retired
owner-private context delivery APIs/carriers must not be reintroduced as scope
target aliases; current owner-quote writers, UI, and specs must use
owner-quote/source-ref names rather than the retired share-context aliases; and
thread subscription constants use
`HumanScopeSubscription*`. Ordinary private-agent conversation writes live in
`internal/store/privateagent/conversation_write.go`, not `space_write.go`; this
path still remains separate from `internal/store/app` because it owns the
message-plan construction boundary for private-agent-visible conversation sends:
durable author vs routing/notification principal separation, freshness/idempotent
replay, owner-private routing, mention delivery, attachment claim echoing,
reply-thread participant/subscription mutation, active-task target projection,
and scope fanout outbox construction. The exit condition is to delete this
privateagent-local writer only after those responsibilities move to a shared
message-domain write service with equivalent tests and guardrails. The guardrail
enforces these touched paths stay free of `SpaceID`, `sourceSpaceKind`,
`SpaceProjection`, `SpaceParticipant`, `space_projection.go`, and
`space_write.go` regressions.

### Private-agent target/send integration test helper scope naming

Private-agent target/send integration tests now assert durable parent writes and
parent-scope membership with scope terminology. Helper parameters and locals in
`internal/api/agent/private_agent_target_send_integration_test.go` use
`wantScopeID`, `parentScopeID`, and `scopeID`; `wantSpaceID`, `parentSpaceID`,
and generic `spaceID` are not allowed to return there. True channel IDs and
workspace IDs remain valid when they model user-facing targets or tenants rather
than storage scope aliases.

### Provider-watch delivery/permission scope columns

Provider-watch current runtime/domain structs, DTOs, frontend types, resolver
names, bootstrap schema, and admin schema use `DeliveryScopeID` /
`PermissionCheckScopeID`, `deliveryScopeId` / `permissionCheckScopeId`, and
`delivery_scope_id` and `permission_check_scope_id`. Old provider-watch column
names are allowed only as versioned migration input or down rollback output.
Resolver names must say `Scope`, not `Space`.

New internal contracts, resolver return types, current DTOs, and
operation-specific domain structs must use `ScopeID` or explicit role names. If
a new field would need both delivery and permission-check semantics, add two
role-named fields instead of another generic id.

### Agent send-param target reference boundary

`message.SendMessageParams` is a current internal agent-send contract. Visible
channel or reply-thread targets must be named `TargetRef`; direct DM sends use
`DMTo`; raw task discussion sends use `TaskScopeID`. The old `SpaceRef` field
and duplicate generic `Channel` fallback have no hard external constraint and
must not return. This boundary does not rename true route/read fields such as
channel route `channelId`, task-board `BoardRef.Channel`, or delivered
`ReceiveActiveTaskTarget.Channel`, where the value really is a user-visible
channel reference rather than an agent send-param fallback.


## Resolver compatibility recovery inventory

The target/access contract forbids using `ErrForbidden` / `ErrNotFound` probing
as the primary way to define target semantics. The remaining recovery paths are
compatibility adapters whose output is still normalized into a typed
`ResolvedScopeTarget` or an existing legacy wrapper over that typed contract:

- **Agent target basis resolver convergence.** The former public-resolver miss
  followed by owner-private agent target recovery has been removed. Agent
  channel/reply-thread write, read, and review-subject target resolution now
  uses one roster-based target-basis resolver over active public/private
  `scope_memberships` rows. Channel reply-thread target access inherits the
  parent channel roster and must not require owner-private root-message read
  visibility; history/search still keep row-level readable predicates after
  target resolution. Reply-thread structure is loaded through a shape-only
  helper; the old structure-plus-public-only access helper is not an allowed
  compatibility surface.
- **Message read-row authority convergence.** History/search/read-check and
  message-derived resource surfaces use the shared SQL readable predicate as the
  production row authority. The Go visible-row layer is only a scanner,
  deduper, pager, DTO mapper, and fail-closed invariant checker; it must not
  reintroduce `VisibilityFilter`-local public/private/owner/grant authorization
  logic or a private-agent target fallback.
- **Legacy send adapter archive recovery.** The older send path can recover an
  archived reply-thread target when the archive storage backend is configured.
  This is a compatibility adapter for already archived reply-thread references,
  not a general target resolver fallback.
- **Error translation at API boundaries.** Compatibility wrappers may translate a
  typed resolver decision into existing `ErrForbidden` / `ErrNotFound` API
  errors after the target roles and permission basis are known.

Any new resolver recovery path must be named in this inventory, guarded by a
bounded predicate, and covered by a focused test or guardrail check.
