# YSD-19106 — Whether a mobile review companion and an offline note queue are justified

- **Status:** approved (2026-08-15)
- **Priority:** EXT — non-blocking for the core, blocking for YSD-19107/19108
- **Decision owner:** @GreyChimp
- **Drafted:** 2026-08-15 by Claude Code (Opus 5)
- **Approval:** ruled by @GreyChimp on 2026-08-15; outcome recorded in
  decision-log.json; review by 2027-08-15

## Question

Phase 5 defers two capabilities behind one condition: build them only if learner
research shows they solve a demonstrated need, and only then go beyond the
policy-safe core. Does the need exist, and are they to be built for the first
release?

## What the gate says today

`capabilityNeedVerdict` over the recorded research
(`src/product/deferred-capability-research.ts`) returns, for both:

| capability              | verdict    | gate standing         | construction    |
| ----------------------- | ---------- | --------------------- | --------------- |
| mobile-review-companion | `unproven` | `decided-after-build` | partially-built |
| offline-note-queue      | `unproven` | `decided-after-build` | built           |

Three facts in that table matter more than the ruling.

**`unproven` is not `not-justified`.** The gate's vocabulary separates "the
evidence shows the core does the job" from "nothing demonstrates it yet, which
is an absence rather than a refusal". The programme holds no learner research
about either capability, so only the second is available, and this decision was
recorded against the verdict the evidence earns rather than the stronger one it
would be convenient to write. The distinction is asserted in
`deferred-capability-research.spec.ts`, in both directions.

**The absence is structural, not a backlog item.**
`planesThatCanDemonstrateAwayFromWorkspaceNeed` returns exactly one plane —
`consented-research`. The need at issue is for work done _away from the
workspace_, and every other observation plane can only see somebody who is in
it: a learner who wanted to review on the train and therefore did not review
emits no session, files no ticket and appears in no funnel. Product analytics
cannot close the gap either, because YSD-17072 permits metrics to carry
`interaction-shape` and `record-shape` and nothing else, so analytics can
establish _that_ work stopped and never _why_. Consented research needs a panel,
and YSD-18002's recruitment has not happened.

**Both gates fired after the build.** The offline note queue shipped under
YSD-15094 and YSD-17034; `mobile-companion` is one of the four rendering
surfaces of the deep-link contract shipped under YSD-15092. The decision this
gate exists to make was, for both, partly made by construction. That is recorded
rather than smoothed over: `gatesDecidedAfterBuild` names them and
`assertGateStillOpen` refuses to pretend the gate can authorize what has already
shipped.

## Recommendation

**Do not build beyond the policy-safe core in the first release**, for both
capabilities, and record the verdict as `unproven` rather than `not-justified`.

## Ruling

Approved as recommended by @GreyChimp on 2026-08-15: **do not build beyond the
policy-safe core in the first release**, for both capabilities.

For the companion, the core already opens the web workspace in a phone browser
under the same rights enforcement, and a companion would move rights-restricted
material and a learner's private notes onto a personal device to serve a need
nobody has observed. For the queue, the ruling governs what comes next; it does
not retroactively authorize what is already here.

Recorded in code as `DEFERRED_CAPABILITY_RULINGS`, with the verdict and the
construction state **as they stood on the day of the ruling**, so
`rulingStanding` reports `superseded-by-evidence` the moment research justifies
either one and `construction-moved` if the tree is built past the ruling. A
ruling that cannot go stale is a sentence; this one reports its own
supersession.

## Options considered

- **Ship the companion anyway** — rejected: it crosses four policy surfaces to
  serve a need with no observation behind it, and "we already mint deep links
  for it" is a fact about a link contract, not about a learner.
- **Record `not-justified`** — rejected: that verdict asserts the core does the
  job, and no evidence says so. Writing it would launder an absence into a
  finding, which is the specific failure this gate is built to catch.
- **Recruit for consented research first** — deferred, not rejected: it is the
  only plane that could settle the question, and it waits on YSD-18002. The
  ruling's review date is what brings it back.

## Consequences

- **YSD-19107's obligation does not arise.** It is conditional on a `justified`
  verdict; `companionObligationArises` returns `false` with the verdict and the
  reason attached, so the absence is a computed answer rather than an assumed
  one.
- **Two crossings on a shipped capability remain unresolved and are now
  visible.** The offline note queue crosses `learner-notes-on-personal-device`
  and `lost-device-revocation`, and neither has a decision from the owner of
  those surfaces. The tree holds controls that bear on them — an offline bundle
  expires locally, a covering grant lapses, accepted records are lease-gated —
  and they are recorded in `NEAREST_CONTROLS` as controls, each with the
  sentence saying why it is not a permission. A time bound limits how long a
  lost device stays useful; it does not revoke anything on demand. **These are
  rights and device-privacy decisions that no amount of demonstrated need would
  settle, and they are owed regardless of this ruling.**
- The first release ships the policy-safe core: the web workspace in a phone
  browser, portable study packages, and deep links.

## Machine-enforced outcome

The gate is asked about the programme rather than about a fixture: its inputs
come from a recorded register instead of an array literal at each call site, and
the owner's ruling is held against the live verdict and the live construction
state every time it is read.

## Control in force

`assertGateStillOpen` throws for both capabilities, and that stays true after
this ruling — approval decides what to build next, not that the gate was
consulted in time. `assertDeferredCapabilitiesJustified` still throws, because
neither verdict is `justified` and this decision does not make one so.
