Deferred from PR #945 (triage after 2 review rounds at head a4d4670046cf94aa1868c6b13065d9aa6ab033b9). One residual finding, reported as round 1 finding 1 and re-confirmed standing in round 2; recorded in llp/0289-sync-lane-asks-the-store-about-hidden-picks.decision.md §not-done.
Finding: on a degraded wizard run, a stale opt-out for a hidden central row makes the sync lane print "nothing syncs" while the daemon ships the row
The lane's hidden-pick check is the policy store's answer, not the export seam's:
src/core/cli/wizard/sync_scope.js:97-99: optedOutAll = new Set(optedOutClientSourceIds(existing)); a hidden pick with any standing local-only entry reads as withheld.
src/core/runtime/source_withhold.js:163: the seam withholds optedOutClientSourceIds(entries).filter((id) => !centralIds.has(id)), so an entry for a central-classified source is inert and the row ships anyway.
The equation "a hidden pick is non-central by construction" holds only while the wizard's locked set actually resolved. Two paths return [] deliberately:
computeLockedSafe swallows a layered-resolution failure (src/core/cli/wizard/index.js:1102, LLP 0182).
- a converge timeout returns
{ status: 'ok', lockedSources: [] } (src/core/cli/wizard/join.js:112, LLP 0129).
Concrete run: the user writes hyp policy client <source> local-only before the org adopts the source (allowed; adoption never clears an existing entry). The org adopts it, so it is central and hidden. On a re-run whose locked resolution fails, the carried hidden row lands in candidatesHiddenIds, reads as withheld, and the lane prints "You picked nothing to record, so nothing syncs to your server." while the daemon, resolving the same config successfully at boot, drops the inert entry and exports every row. A false claim in the LLP 0188 #never-silent direction, on a privacy-facing screen.
Why it was deferred, not fixed
The wizard cannot subtract central ids on the very run where computing them is what failed, and it cannot treat "no locked rows at all" as the degraded signal, because that is also the healthy shape of an enrolled machine whose org configures nothing (the exact case LLP 0289's main test covers). The corner needs a resolution failure, a stale entry, and a hidden central row at once.
What would close it
Give the sync lane a way to tell "no central rows" from "could not tell": a signal from the orchestrator (e.g. a lockedResolved/degraded flag alongside lockedSources) that the lane can use to fall back to the conservative "still syncs" sentence when the central set is unknown. Introducing that signal is a design decision about the degraded path, so it should get its own LLP (extending LLP 0289 §not-done, and touching LLP 0182 / LLP 0129 rationale) before the code change.
Backlink: PR #945.
Deferred from PR #945 (triage after 2 review rounds at head
a4d4670046cf94aa1868c6b13065d9aa6ab033b9). One residual finding, reported as round 1 finding 1 and re-confirmed standing in round 2; recorded inllp/0289-sync-lane-asks-the-store-about-hidden-picks.decision.md§not-done.Finding: on a degraded wizard run, a stale opt-out for a hidden central row makes the sync lane print "nothing syncs" while the daemon ships the row
The lane's hidden-pick check is the policy store's answer, not the export seam's:
src/core/cli/wizard/sync_scope.js:97-99:optedOutAll = new Set(optedOutClientSourceIds(existing)); a hidden pick with any standinglocal-onlyentry reads as withheld.src/core/runtime/source_withhold.js:163: the seam withholdsoptedOutClientSourceIds(entries).filter((id) => !centralIds.has(id)), so an entry for a central-classified source is inert and the row ships anyway.The equation "a hidden pick is non-central by construction" holds only while the wizard's locked set actually resolved. Two paths return
[]deliberately:computeLockedSafeswallows a layered-resolution failure (src/core/cli/wizard/index.js:1102, LLP 0182).{ status: 'ok', lockedSources: [] }(src/core/cli/wizard/join.js:112, LLP 0129).Concrete run: the user writes
hyp policy client <source> local-onlybefore the org adopts the source (allowed; adoption never clears an existing entry). The org adopts it, so it is central and hidden. On a re-run whose locked resolution fails, the carried hidden row lands incandidatesHiddenIds, reads as withheld, and the lane prints "You picked nothing to record, so nothing syncs to your server." while the daemon, resolving the same config successfully at boot, drops the inert entry and exports every row. A false claim in the LLP 0188 #never-silent direction, on a privacy-facing screen.Why it was deferred, not fixed
The wizard cannot subtract central ids on the very run where computing them is what failed, and it cannot treat "no locked rows at all" as the degraded signal, because that is also the healthy shape of an enrolled machine whose org configures nothing (the exact case LLP 0289's main test covers). The corner needs a resolution failure, a stale entry, and a hidden central row at once.
What would close it
Give the sync lane a way to tell "no central rows" from "could not tell": a signal from the orchestrator (e.g. a
lockedResolved/degraded flag alongsidelockedSources) that the lane can use to fall back to the conservative "still syncs" sentence when the central set is unknown. Introducing that signal is a design decision about the degraded path, so it should get its own LLP (extending LLP 0289 §not-done, and touching LLP 0182 / LLP 0129 rationale) before the code change.Backlink: PR #945.