Files
pad/internal/server
xarmian ee607047dd feat(copy): the two cross-workspace doors resolve referents, with their pin (TASK-2878)
PLAN-2857 U1, doors seven and eight. `migrateCopyFields` and
`handleCopyItemPreflight` now take their relation decision from
`store.MigrateRelationReferents` — the same function the four write doors
and the two move doors already call, which is the entire reason it exists.

The defect this closes: MigrateFields matches on key and TYPE, so a
same-named `relation` field carried a SOURCE-workspace item id across the
boundary and the preflight reported it as a clean carry. What landed in
workspace B was a value naming a row in workspace A — unrenderable, and
indistinguishable on read from a legitimate reference.

Provenance decides, as at the move doors. A CARRIED value on a
cross-workspace copy is dropped without a lookup (no id from A can mean
anything in B) and reported through the `dropped_fields` channel BUG-2674
established; a SUPPLIED override is an ordinary write and an unresolvable
one is refused, 400 validation_error on both doors, rendered by the same
`store.RelationIssuesMessage` so one refusal cannot acquire two phrasings.
`internal/server`'s `relationIssuesMessage` now delegates to it: the eighth
door refuses from inside `store`, so the sentence had to be reachable there.

Two things are threaded rather than re-derived, and both are load-bearing:

- The TRANSACTION, not the pool. `migrateCopyFields` becomes a method
  taking a Queryer, and `copyItemAcrossWorkspacesTx` passes its `tx`. That
  function has held a transaction since its second statement, so a pool read
  from inside it can wait for a free connection while every pooled
  connection is blocked on this transaction's locks — the starvation shape
  BUG-2409 fixed for the attachment planner and this repo keeps a
  deterministic test for. This is what the day-70 handoff named as the
  reason these two doors were not wired with the other six.
- The MODE comes from the `scope` MigrateFields was already given, not from
  a second boundary test. Two independent answers to "is this crossing a
  workspace" is how one request gets migrated one way and validated the
  other, and this path also serves a copy whose target IS the source
  workspace, where relations resolve and survive exactly as on a move.

The destination workspace id is the resolution scope: a supplied override
is a write into B and must name something that exists there.

THE PIN, and why it is not a per-door table. These two doors sit in
different PACKAGES and the code at both sites says so is how they drift
unnoticed. A table with a row per door can be fully green while the two
disagree about one request, which is the defect rather than a gap in
coverage of it. So every case sends ONE body to BOTH endpoints:

- carried relation — must drop on both, and the preflight must say
  referent_not_portable rather than the generic no_target_field, which is
  false here (the destination DOES declare the key, so that answer sends
  the reader to fix a schema that is fine);
- supplied + unresolvable — both refuse, same status, same code, both name
  the offending field and value, and nothing is written;
- supplied + resolvable — the positive control, supplied as a REF so
  resolution is visible in the result. Without it the first two legs are
  equally consistent with "relations always fail".

Negative controls run, all three mutants BUILD-CHECKED first (a
non-compiling mutant produces no `--- FAIL` lines and reads as survived):
both doors unwired = DETECTED; preflight unwired alone = DETECTED; store
unwired alone = DETECTED. Each single-door mutant failing is the pin's
whole claim — neither door can be wired without the other.

CONVE-23 sweep: the preflight's LIMITATION comment said this gap belonged
in MigrateFields "for both callers at once". That is now false in its
prescription as well as its premise — `internal/items` is DB-free by
construction and cannot ask whether a string names a live item — so the
comment records where the fix actually went and what of it remains open
(`computed`, `terminal_options`, `unique_scope`).

Gates: internal/server ok 170.0s · internal/store ok 296.1s · internal/items
ok · go vet clean · gofmt clean · make lint 0 issues.
2026-09-04 15:40:55 +00:00
..