The sync gate's hidden-pick sentence consults the policy store (LLP 0289) - #945
The sync gate's hidden-pick sentence consults the policy store (LLP 0289)#945philcunliffe wants to merge 4 commits into
Conversation
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… the hidden-pick decision in the span Self-review of PR #945 found two items. 1. medium, recorded not fixed: `hiddenCandidateSyncs` reads the client policy store, but the export seam withholds `optedOutClientSourceIds(entries)` minus the central-classified ids. A hidden pick is non-central by construction only while the wizard's locked set resolved, and `computeLockedSafe` (LLP 0182) and a converge timeout (LLP 0129) both return `[]` on purpose. On such a run a hidden central row with a stale opt-out reads as withheld and the lane prints "nothing syncs" while the daemon exports it. Closing it needs the lane to tell "no central rows" from "could not tell", a signal the orchestrator does not send; that is a design change, so LLP 0289 #not-done records the corner and the code says which case it is. 2. low, fixed: the branch the store read decides was invisible in telemetry. `wizard.sync_scope.finish` now carries `hidden_picks` and, on the no-candidates path, `hidden_picks_syncing`, so "it said nothing syncs but rows shipped" is triageable from the signal. Counts and a boolean, never the ids. npm test 4533 pass / 0 fail; npm run typecheck clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review record: PR #945 @
|
Master landed PR #925 (LLP 0281 #visible-org-row), which added the sibling no-candidates branch this PR's LLP 0289 #not-done anticipated. Both sides touch the same few lines of the sync lane, so the merge is where the two branches either agree on one fact or drift apart. Resolution: - `src/core/cli/wizard/sync_scope.js`: master's org-row branch gated on `candidatesHidden > 0`, the count this PR replaces with `candidatesHiddenIds`. It now reads the same `hiddenCandidateSyncs` boolean its no-locked sibling reads, which is exactly what LLP 0289 #not-done said it should do when #925 landed. A hidden pick the store already withholds does not ship, so the exhaustive fleet sentence is true again and the two branches turn on one question rather than disagreeing about it. Both branches of that fork now record `hidden_picks_syncing` on the span. - `llp/0276-...decision.md`: both sides appended an `Extended-by:` note to #no-candidates. Both are kept, master's (LLP 0281) first. - `test/core/cli/wizard/sync_scope.test.js`: both sides' tests are kept, master's two ported to `candidatesHiddenIds`, plus a new pin for the case the merge creates - an org row beside a hidden pick already opted out keeps the exhaustive sentence. That pin fails on the mechanical rename and passes on this resolution. - `llp/0281-...decision.md`: forward-ref to LLP 0289 #ask-the-store on #visible-org-row, since its branch now carries that `@ref`. - `llp/0289-...decision.md`: #not-done records that LLP 0281 has landed and the residual is closed. npm test: 5017 pass / 0 fail / 1 skipped. npm run typecheck: clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e it is Self-review of the master merge found one finding, fixed here. medium: the merge re-gated LLP 0281 #visible-org-row's branch on `hiddenCandidateSyncs`, a question about shipping, when what that Accepted decision settled was a question about ownership. It says so itself: "no clause here is affirmatively false about *what* leaves the machine - which is why round 2 left it", and the rule it adopted is "a sentence names only the rows whose owner it names." So on an enrolled machine with a visible org row, a hidden carried pick (LLP 0202 #carry-through), and a standing `hyp policy client raw-anthropic local-only`, the merged lane printed "Everything you picked is managed by your fleet and always syncs." over a row the fleet does not manage and that does not sync - the exact claim #925 had just removed from master, restored by this branch's merge and pinned by a new test. The branch now splits the two claims: - the fleet sentence narrows whenever a hidden pick exists, withheld or not, which is what LLP 0281 settled and what master does today - the second line, the one that promises an export, is the store's answer and prints only when a hidden pick is not already withheld, the same question the no-locked sibling asks So the two branches still agree about what leaves the machine, without this one taking back an owner's claim it had given up. The withheld row is still never named. LLP 0289 #not-done and #consequences record which half of that branch the store governs; the `Extended-by:` blockquote this PR appends to LLP 0281 #visible-org-row is corrected to match. Nothing LLP 0281 settled is edited. The reworked test fails on the pre-fix lane ("Everything you picked is managed by your fleet and always syncs." where none of it is the fleet's) and passes after. npm test: 5016 pass / 0 fail / 1 skipped (the one failure in this sandbox, `hyparquet-floor-pin`, reproduces unmodified at 9549f38 and is a shared-node_modules artifact). npm run typecheck: clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review record: PR #945 @
|
|
Deferred residual findings tracked in #970. Triage at head |
Closes the deferred review finding triaged out of PR #925 (reported in both review rounds: round 1 finding 3, round 2 finding 2). One behaviour fix with regression tests, plus the design change it needed: new decision doc LLP 0289, with an
Extended-by:forward-ref appended to LLP 0276 §no-candidates (Accepted, so nothing it settled is edited).The finding: the sync gate's hidden-pick sentence did not consult the policy store
src/core/cli/wizard/sync_scope.jsOn the empty-candidate path with no visible org row, the lane printed
whenever
candidatesHidden > 0. That count only says a display-filtered picked row exists; the sentence claims it ships.Root cause
A hidden row is off every wizard screen (LLP 0202, LLP 0276) but it is still a picker source with an id, and
hyp policy client raw-anthropic local-onlyaddresses it by that id. The entry lands in the client-sync store, which is exactly what the export seam reads:buildSourceWithholdResolverwithholdsoptedOutClientSourceIds(entries)minus the central-classified ids (src/core/runtime/source_withhold.js:129-165). The lane's only store-derived set,optedOutBefore, is computed overcandidateIds, i.e. the visible candidates (sync_scope.js:80-81), so a hidden id was invisible to it. A user with a standing opt-out was told their capture syncs while the seam was already withholding all of it, on a privacy-facing screen (LLP 0188 #never-silent).Why it needed an LLP first
The count is LLP 0276 #no-candidates' own design: "They are counts, not lists: the lane must be able to tell the truth about them without being able to name them." With no id there is nothing to look up, so the fix is a design change, which is why PR #925 deferred it.
LLP 0289 §ask-the-store settles it: the hidden picks cross into the lane as ids, and the lane spends them on the store rather than on the screen. Naming a hidden row stays forbidden (asserted by tests, before and after); what the lane gains is the ability to ask about a row it may not print. A privacy screen denied the identity of the thing it describes cannot check its own claim, and a wrong claim costs more (LLP 0188 #never-silent) than the lane holding an id it never renders (LLP 0202).
The hidden locked rows keep their count, deliberately: an org row always syncs (LLP 0188 #locked) and the export seam drops opt-out entries for central-classified sources, so a store entry for one is inert and that sentence is unconditional because the fact is. The asymmetry between the two fields is the asymmetry between the two kinds of row.
The rejected alternative is recorded too: having the orchestrator pre-answer the question keeps the count but makes the wizard a second reader of a privacy store the lane already reads, with a different failure mode (the lane degrades a corrupt store to a warning and a skipped step; a read in the orchestrator would throw out of the whole run).
The fix
RunWizardSyncScopeOptions.candidatesHidden(count) becomescandidatesHiddenIds(src/core/cli/wizard/types.d.ts);lockedHiddenis unchanged.src/core/cli/wizard/index.jspasses the ids the display filter dropped frompicked.descriptors.runWizardSyncScopereads the store's full opted-out set (not only the slice covering the visible candidates) and fires the sentence only when at least one hidden pick has no standinglocal-onlyentry. Every hidden pick withheld means nothing leaves the machine, so the branch reads the fourth sentence, "You picked nothing to record, so nothing syncs to your server.", which is now the true one. The check is "any hidden pick ships", never "every one does".The visible-candidate path is untouched:
optedOutBeforestill governs what the gate and menu render and what the write keeps, because those screens edit only what they show. The path staysnoQuestionand writes nothing, so LLP 0191 #back-edges and the step counter are unaffected.The other branch the issue names (
sync_scope.js:126)The issue asks for one change covering both
:107and:126.:126is LLP 0281 §visible-org-row, which is on PR #925's branch and not onmaster, so it is not in this diff. This PR mints thecandidatesHiddenIdsanswer that branch needs; when #925 lands, its condition consults the same boolean and the two branches keep agreeing. Recorded in LLP 0289 §not-done, and the merge point is the same few lines, so the conflict resolution is mechanical.Testing
The load-bearing regression test drives the real sync lane through
runInitWizard, so it asserts on what the user reads rather than on the lane's inputs and is therefore independent of the options-shape change (a lane-level test written against the new field would pass vacuously on pre-fix code, which ignores the field).runInitWizard: a hidden picked row with a standing opt-out does not make the lane claim capture syncstest/core/cli/wizard/index.test.jsrunInitWizard: a hidden picked row with no opt-out keeps the sentence that says capture still syncszero visible candidates with a hidden picked row already opted out: says nothing syncstest/core/cli/wizard/sync_scope.test.jszero visible candidates with one hidden pick opted out and one standing: does not claim nothing syncsa stale opt-out for a hidden locked row does not soften the fleet sentenceThe failing-before observation, at
origin/masterwith only the test added:After the fix that line reads
You picked nothing to record, so nothing syncs to your server.Re-verified by revertingsync_scope.jsandindex.jsin place and re-running the two files.Every case also asserts the withheld row is still never named, opted out or not.
npm test: 4533 pass / 0 fail / 1 skipped.npm run typecheck: clean.Fixes #935