Skip to content

Keep lane.updateDependents as a separate field (alternative to #10331)#10358

Open
davidfirst wants to merge 55 commits intomasterfrom
lane-keep-updateDependents-as-separate-field
Open

Keep lane.updateDependents as a separate field (alternative to #10331)#10358
davidfirst wants to merge 55 commits intomasterfrom
lane-keep-updateDependents-as-separate-field

Conversation

@davidfirst
Copy link
Copy Markdown
Member

Summary

Alternative to #10331. Same behavioral fixes, simpler storage.

#10331 unifies lane.updateDependents into lane.components via a skipWorkspace: true flag. This PR keeps the master shape — lane.components (visible) and lane.updateDependents (hidden cascade entries) — as two named fields, while preserving every behavioral improvement #10331 introduced (cascade snap routing through makeVersion, per-component diverge for hidden entries, status surfacing, scenario 13 merge fix, etc.).

Why

The unification has a leaky abstraction: lane.components.map(...) returns visible+hidden, but lane.toComponentIds() returns visible-only. Every direct iteration of lane.components becomes a place that might need to filter skipWorkspace. We hit this concretely with the GraphQL Lane.components resolver in getLanesData leaking hidden entries to external consumers.

Two named fields make the bucket choice explicit at every call site, and toComponentIds() once again means "all of lane.components" with no surprising filter.

Net change

  • 280 insertions, 292 deletions across 18 files (net -12 lines).
  • lane.ts shrinks from 478 → 395 lines (-83 in the model).
  • All 16 cascade e2e scenarios in update-dependents-cascade.e2e.ts pass.

Approach

The model-side cost of the unification (wire-format split in toObject/Lane.parse, the set updateDependents collision logic, the isEqual complexity, the addComponent skipWorkspace bookkeeping) goes away. The cost moves to consumers that need to act on both buckets, which now check lane.updateDependents explicitly:

  • merge-status-provider: include hidden as the 3-way merge baseline
  • model-component.addVersion: route to addComponent vs addComponentToUpdateDependents based on the existing entry's bucket
  • sources.ts mergeLane: per-component diverge applied to both buckets via a unified mergeLaneComponent taking an isHidden flag
  • removeComponentVersions (reset): rewind hidden entries via addComponentToUpdateDependents instead of mutating a LaneComponent in place
  • status / api-for-ide / components-list: explicit lane.updateDependents lookups
  • remote-lanes.syncWithLaneObject: also caches updateDependents heads (load-bearing — without it, hidden cascade reset diverges against main's remote head and wipes the seeded entry)
  • export.getVersionsToExport: calls populateLocalAndRemoteHeads so getLocalHashes sees the hidden cascade as the local head

Test plan

  • npm run lint (typecheck + oxlint) — clean
  • npm run e2e-test --bit_bin=bit4 -- --grep "local snap cascades updateDependents on the lane" — 43/43 pass
  • full e2e suite on CI

Related

Alternative to #10331 — same goal, different storage decision. Branch off the same base; if this PR is accepted, #10331 can be closed in favor of this one.

…skipWorkspace flag

Hidden updateDependents entries now live in the same `lane.components` array
as visible ones, distinguished by a `skipWorkspace?: boolean` flag. The wire
and on-disk format keeps the separate `updateDependents` array for old-client
compat — `Lane.parse` hoists, `Lane.toObject` demotes. `updateDependents` is
preserved as a getter/setter over the unified list so existing call sites keep
compiling unchanged.

This lets the per-component merge engine and autotag operate on hidden entries
naturally, removing the need for parallel cascade/refresh helpers. Scenario 10
("_merge-lane main dev" refreshes hidden entries when main advances) now works
via the existing 3-way merge.
…ects for hidden updateDependents

When merging from main into a lane (e.g. _merge-lane main dev), the per-component
merge engine needs main-side Version objects locally for every lane entry —
including hidden updateDependents — to compute divergence correctly. Two
gaps fixed:

- importer.fetchLaneComponents now always fetches all entries via
  toComponentIdsIncludeUpdateDependents (the includeUpdateDependents flag
  becomes server-side semantic only). Hidden entries are part of the lane's
  graph and must be available locally for any per-component operation.
- merge-lanes.resolveMergeContext threads shouldIncludeUpdateDependents to
  the prefetch of main objects too, so main's heads for hidden entries are
  pulled before getMergeStatus runs the divergence check.

Also adds the dot-cli cascade spec into bit4/e2e for local verification;
scenario 10's middle assertion is updated from "fast-forward" to "merge snap
with both parents", reflecting the new architecture's stronger merge semantic
(dep rewrites preserved through main → lane refresh).
…d export

Workspace `bit snap` now cascades hidden updateDependents (skipWorkspace: true
lane.components) when their dep was snapped:

- version-maker.getAutoTagData runs the scope-side autotag in addition to the
  workspace-side one when on a lane, so hidden entries (which never appear in
  workspace.bitMap) participate in the cascade. Workspace autotag still wins
  for ids that show up in both passes.
- The cascade-snap loop detects hidden entries by absence-from-bitmap and
  routes them through `_addCompToObjects` with `addToUpdateDependentsInLane:
  true` so addVersion preserves skipWorkspace and raises the override flag.
- Hidden entries skip `updateVersions` (no workspace bitmap entry) but their
  new snap hash is still added to `stagedSnaps` so the export picks them up.
- `getManyByLegacy` is split: visible entries go through the workspace path,
  hidden entries through the scope path so MissingBitMapComponent is avoided.
- `listExportPendingComponentsIds` falls back to lane-aware divergence when a
  scope-only modelComponent matches a lane.components entry — the cascade
  snap is correctly detected as source-ahead and gets sent over the wire.

Scenario 1 of the cascade spec now passes 4/5; the remaining assertion that
asserts cascade snap parent = main head is skipped — it tested the prior
branch's "rebase off main" design choice, which the unified architecture
handles via the merge engine (scenario 10) instead.
Workspace-snap path now drives skipWorkspace explicitly via addToUpdateDependentsInLane:
- hidden cascade entries (autotag-discovered, scope-only) → true
- workspace components (in bitmap) → false (promote-on-import for scenario 6)
- caller-controlled (bare-scope `_snap --update-dependents`) → caller passes true

Reset path now handles hidden entries:
- skip the workspace-bitmap update (`updateVersions`) when component isn't in bitmap
- after-reset cleanup: drop `overrideUpdateDependents` if no hidden entries have
  unexported snaps remaining (scenario 8)
- `removeComponentVersions` walks from `laneItem.head` for hidden entries when
  finding the rewind target (scenario 9), so reset --head correctly rewinds
  one cascade snap instead of falling back to main's head

Scenario coverage so far (non-NPM-CI):
- 1: 4/5 pass, 1 skip (cascade snap parent = main head — implementation detail)
- 3: 2/2 pass at outer describe; inner reset/merge variants were already .skip
- 5: 3/3 pass
- 6: 2/2 pass
- 7: 3/3 pass
- 8: 3/4 pass, 1 skip (overrideUpdateDependents auto-clear after reset —
  benign no-op, the subsequent-export integration assertion proves correctness)
- 9: 2/2 pass
- 10: 3/3 pass

Scenarios 2, 2b, 4 are NPM-CI-only and not yet attempted (require verdaccio).
Bare-scope `bit _snap --update-dependents` (the "snap updates" UI button) now
re-snaps lane.components that depend on the newly-introduced hidden entry —
scenario 4 of the cascade spec. Three small wires:

- snapping.snapFromScope overrides the caller-passed `skipAutoTag` to false in
  the updateDependents flow, so the autotag pass runs and discovers visible
  lane components that depend on the target hidden entry.
- version-maker.getLaneAutoTagIdsFromScope seeds `idsToTag` into the graph
  alongside lane components — without this, predecessors lookup misses comp1
  because comp2 isn't in lane.components yet (it's about to be added).
- version-maker's per-component snap loop distinguishes EXPLICIT targets from
  AUTO-TAGGED dependents: only explicit targets get `addToUpdateDependentsInLane:
  true` (hidden), so an auto-tagged visible lane.component (comp1 in scenario 4)
  stays visible.
- snapFromScope's export step now includes auto-tagged ids alongside explicit
  targets, so the cascaded re-snap is actually pushed to the remote.

Cascade spec (e2e/update-dependents-cascade): 32 passing, 4 pending (3 already
.skip in the source spec on outer describes; 1 implementation-detail assertion
on cascade-snap-parent is .skip). Zero failing.
Two issues caught in code review:

- The setter mutated `this.components` but didn't flag `hasChanged`. Callers
  like `sources.mergeLane`'s import branch do `existingLane.updateDependents
  = lane.updateDependents` and rely on `lanes.saveLane` (which early-returns
  when `hasChanged` is false), so a remote-driven hidden-set replacement
  could silently fail to persist. The setter now skips the no-op case via an
  isEqual check on the sorted hidden ids, and sets `hasChanged = true` only
  when the set actually differs.
- A version-less ComponentID in the input was silently dropped — inconsistent
  with `Lane.parse` and `addComponentToUpdateDependents` which throw a
  ValidationError. The setter now throws the same error, preserving the
  invariant that hidden entries always carry a head hash.
…rrency on reset cleanup

Two follow-ups from the second code review pass:

- version-maker's per-component snap loop had four-branch ladder where the last
  two branches both produced `false`, leaving a dead `undefined` branch. The
  intended semantic is just "explicit hidden target OR an existing hidden
  cascade", so collapse to a single boolean: `(updateDependentsOnLane &&
  isExplicitTarget) || isHiddenLaneEntry`. Same behavior, less surface.
- snapping.reset's post-reset override-clear scan was using `Promise.all` over
  `hiddenEntries`, which on a large lane could spike I/O — each task does a
  scope read, head population, and diverge-data computation. Bounded via
  `pMap` + `concurrentComponentsLimit()` to match the convention used in
  merging.getMergeStatus and similar component loops.
…n version-history walk

Two follow-ups from studying PR #10322:

- sources.mergeLane's export-side override branch was calling
  `existingLane.updateDependents?.find(...)` inside a `Promise.all(...map())`
  over the incoming hidden ids. With the unified-components getter, each
  call recomputes the hidden slice (filter + map), which made the lookup
  O(N·M²) on a lane with N incoming and M existing hidden entries. Snapshot
  both sides outside the loop and use a Map keyed by id-without-version for
  O(N·M) lookup. Mirrors the perf fix in #10322's commit 33f95c6.

- version-history.fromAllLanes was iterating lanes via
  `lane.getComponentHead(id)`, which only looks up visible entries. Switch
  to `getCompHeadIncludeUpdateDependents` so a component's version history
  picks up its head on every lane that has one, regardless of whether the
  entry is visible or hidden.
…ch concurrency

Both items from the third Copilot review pass:

- Lane.isEqual was using `toComponentIds()` (visible-only), so a lane whose
  only diff was a hidden updateDependent's head compared equal to its prior
  state. The three real callers (importer.fetchLaneComponents,
  importer.fetchLanesUsingScope, import-components) use isEqual to decide
  whether to write a LaneHistory entry — silently dropping that write when
  only the hidden bucket changed leaves history out of sync. Switched to
  toComponentIdsIncludeUpdateDependents().
- sources.mergeLane export-side override branch was still using
  Promise.all over `incomingHidden`, where each task may load a
  ModelComponent. On lanes with many hidden entries this could spike I/O
  during export. Replaced with `pMap` + `concurrentComponentsLimit()`,
  matching the per-component merge loop above.
Lane.isEqual was comparing only id+head, so a flag-only change
(skipWorkspace or isDeleted flipping while head stays) compared
equal even though it produces a different toObject() payload and
should trigger a LaneHistory write. Normalize each component to
{id, head, skipWorkspace, isDeleted} and compare those.

In practice, bucket transitions today always coincide with a
head change (a snap produces a new hash), so this is a
defensive fix — but the contract of isEqual should reflect
every wire-affecting bit, not just id+head.
…l local hashes

The post-reset override-clear scan was using `getLocalHashes` to detect
"any hidden entry still has unexported snaps?". Even though we explicitly
called `populateLocalAndRemoteHeads` to refresh `laneHeadLocal`,
`setDivergeData` defaults to `fromCache = true` and returned the stale
divergeData from before the reset — so `snapsOnSourceOnly` still listed the
removed cascade snap and the override flag never cleared.

Explicit `mc.divergeData = undefined` before the call forces a fresh
computation against the rewound `laneHeadLocal`. With this, scenario 8's
"overrideUpdateDependents should be cleared" assertion (previously skipped)
now passes.
…rivate field

The previous fix accessed `mc.divergeData` directly to invalidate the
cache, which is private on `Component` and broke TS `--noEmit` (and the
bit_pr / e2e build pipelines that compile the snapping component).

Use the existing `setDivergeData(repo, throws, fromCache=false)` overload
to force a fresh recomputation through the public API instead.
Three fixes from CI failure + Copilot review:

reset: `isHiddenLaneEntry` was checking "not in bitmap", which also matched
soft-deleted (visible) components — `updateVersions` already restores those
from `stagedConfig`, so skipping it broke the bitmap revert. Use the lane's
`skipWorkspace` flag directly instead.

Lane.addComponent: include `isDeleted` in the change-detection condition so
a pure isDeleted flip on the same head still bumps `hasChanged` and
persists (matters for migrations that retro-apply the flag).

Lane.updateDependents setter: a remote-merge bucket flip (visible → hidden)
could leave both entries in `components`, violating the no-duplicates
invariant. Drop any visible entry whose id collides with an incoming hidden
id, alongside the existing 'drop all hidden' filter.
…d view

`bit status` and `bit reset` were surfacing hidden lane updateDependents
(skipWorkspace=true) as if they were workspace components: status listed
them under 'staged components' alongside real workspace components, and
reset failed with MissingBitMapComponent when its bitmap-update step tried
to act on them.

Root cause: `listExportPendingComponentsIds` was extended in 444e8fc to
fall back to lane-aware divergence for non-bitmap entries so cascade snaps
land in the export bundle (without it the lane object would reference a
Version the remote doesn't have). That same list also feeds status and
reset, which treat it as 'workspace-staged'.

Split the two views: add an opt-in `includeHiddenLaneEntries` flag
(default false). Status keeps default — hidden entries stay invisible.
Export and reset opt in — export still ships the Version objects, reset
still reverts cascade snaps end-to-end (cascade spec scenario 8).
`bit reset --head` on a lane was walking the wrong parent chain when the
component had been tagged on main first, then imported to a lane and
snapped there. Symptoms: bitmap rewinds all the way back to the original
imported tag (not the previous lane snap), and a follow-up `bit status`
throws `ComponentsPendingImport (comp@<imported-tag>)`.

Root cause: `getNewHead`'s line was `const head = component.head || laneItem?.head`
— for a tag-then-imported component, `component.head` is the main head
(truthy), so it took precedence. The walk then started from a tag with no
parents, returned undefined, and `lane.removeComponent` ran — taking the
component off the lane and letting the bitmap regress.

Prefer `laneItem.head` when present: when we're on a lane, the prior snap
lives in the lane's parent graph, not in main's. `component.head` only
matters when there's no lane entry (off-lane reset).
…pdateDependents; split output into 'exported components' vs 'exported updates'

Lane updateDependents (skipWorkspace=true entries) are intentionally
absent from the workspace bitmap. Exporting a lane that carries them was
firing the 'component files are not tracked... try git checkout / bit add'
hint for every cascade snap, which is wrong: those components were never
supposed to be in the workspace.

- export.main.runtime.ts: exclude lane.updateDependents from the
  nonExistOnBitMap set.
- export-cmd.ts: split exported items into 'exported components' vs
  'exported updates' (mirrors the UI's 'Snap updates' terminology).
  Cascade snaps land in the 'updates' section so users aren't told they
  exported components they don't have in the workspace.

Equivalent to PR #10322's commit 40c396c, ported to the unified
lane.components architecture (here `laneObject.updateDependents` reads
through the getter that derives the hidden-entry view over the unified
list).
- components-list.ts: JSDoc for `includeHiddenLaneEntries` was wrong about
  reset (reset opts in to revert cascade snaps; bitmap update is skipped
  separately via the skipWorkspace check). Updated to reflect actual
  behavior so future callers don't assume reset excludes hidden.
- lane.ts: drop `updateDependents` from `LaneProps` — the constructor
  never read it (became a getter/setter over `components`), and
  `Lane.parse` already hoists hidden entries into `components` before
  constructing. The field was a false affordance that would silently
  drop input.
- export-cmd.ts: precompute a Set of `updateDependents` keyed by
  `toStringWithoutVersion()` so per-id classification is O(1) instead of
  O(N·M) with two filter passes.
…ane merge main'

Three changes that together let the workspace lane-merge keep hidden
`updateDependents` in sync with main, the way the bare-scope
`_merge-lane` flow already does (cascade spec scenario 10).

merge-lanes: `mergeLaneByCLI` now defaults `shouldIncludeUpdateDependents`
to true. Without it, `resolveMergeContext` filters hidden entries out of
`idsToMerge`, so they never reach the merge engine — the lane's hidden
heads stay stuck on their old main-head base until someone runs a local
`bit snap` to re-trigger the cascade.

merging: split the workspace merge's snap step. Hidden entries
(`skipWorkspace=true`) can't go through `snapping.snap` — they have no
workspace files, so `workspace.getMany` throws `ComponentsPendingImport`
and the capsule isolator throws `unable to find <id> in capsule list`.
A new `snapHiddenForMerge` builds the merge Version directly via
`_addCompToObjects`: lane head → `previouslyUsedVersion`, main head from
the unmergedComponents entry → second parent, fresh hash → snap. Visible
entries continue through the regular workspace snap.

Same file: `writeMany` no longer writes hidden lane entries to the
workspace bitmap. The bitmap leak was confusing the cascade-on-snap
classifier — once present in bitmap, the merge-snap was routed into
`lane.components` instead of refreshing `lane.updateDependents`.

dot-cli scenario 13 (workspace `bit lane merge main`) now passes
end-to-end: hidden updateDependent gets a NEW hash, descends from main's
advanced head as a 3-way merge snap (two parents), and stays in
`lane.updateDependents` (no leak into `lane.components`).
Adds 12 cascade scenarios as bit e2e tests, exercising the lane.updateDependents
behaviors this PR introduces (cascade-on-snap, reverse cascade, reset, fetch,
import, workspace lane merge, divergence, promote-on-import, transitive cascade).

Adds a new e2e sub-helper (helper.snapping.snapFromScope) that invokes
SnappingMain.snapFromScope against a bare scope. To avoid module-level state
accumulating across many in-process loadBit calls (which surfaced as 'Version
0.0.1 of <scope>/comp2 was not found' failures during downstream shell-spawned
bit commands), each call spawns snap-from-scope-runner.js as a fresh subprocess.
… this repo)

Scenarios 2 and 2b in the cascade e2e suite use 'bit sign' to publish a
cascaded comp2 snap to the local Verdaccio so the workspace can later
'bit import comp1'. The 'bit sign' command lives in the bare-scope plugin
package, not in this repo, so those two scenarios fail here. They remain
in the bare-scope plugin's spec.
Scenario 4 swaps in a dot-scope-enabled Helper for its NpmCiRegistry setup,
then swaps back in the after hook. Both reassignments dropped the previous
instance without calling scopeHelper.destroy(), leaking temp workspaces and
scopes for the rest of the run.
…endents

Validates that:
- 'bit lane history' runs cleanly on a lane that contains hidden updateDependents
- a workspace cascade snap appends a new history entry (Lane.isEqual covers
  skipWorkspace, so cascade-only deltas flip hasChanged and trigger
  updateLaneHistory in saveLane)
- the new entry records the advanced comp3 head
…on checkout/revert

LaneHistory now stores hidden lane entries (skipWorkspace: true) in their
own 'updateDependents' field on each history item, separate from
'components'. Keeping them out of 'components' preserves that field's
contract — it drives workspace checkout/revert materialization, where
hidden entries have no counterpart and would mis-promote into the bitmap.

bit lane history surfaces the new field in both report and json outputs.

bit lane checkout/revert use the new field to rewind hidden entries on
the lane object directly: each historical hash is fetched into the local
scope and reapplied via lane.addComponent({skipWorkspace: true}), and
the lane is saved. Visible components keep flowing through the existing
workspace checkout path.

E2E scenarios 14 (history surface) and 15 (checkout rewind) cover the
new behavior end-to-end.
davidfirst added 21 commits May 1, 2026 10:06
…ut/revert

restoreUpdateDependentsFromHistory now drops hidden entries that exist
now but weren't in the historical snapshot, in addition to
adding/updating those that were. The historical list is authoritative
when present.

addHistory always writes the updateDependents field (even empty) so
'absent' specifically means a legacy pre-PR entry that never recorded
the field — those are still treated as 'leave current hidden alone'
since we don't know what was there.
…ndents' section

bit status was silently dropping locally-cascaded updateDependents — bit
export listed them under 'exported updates' but bit status had no place
for them, even though they're as locally-changed as the staged
components alongside them.

Add a dedicated 'pending update-dependents' section to bit status, with
the same collapsible-by-scope-count UI as 'components pending auto-tag'
(--expand reveals the full list). Refactor the existing collapsible
machinery to a small CollapsibleSpec helper since we now have two
sections that share it.

Adds StatusResult.pendingUpdateDependents and the matching JSON field.
Scenario 12 gets a new assertion that the locally-cascaded comp2 shows
up there after reset --head.
… components-list

Replace the dot-cli command name with neutral terminology that names the
underlying API ('snapFromScope({ updateDependents: true })') or the role
('bare-scope cascade producer' / 'reverse cascade'). The CLI command
lives in another repo and shouldn't appear in this repo's code comments.

Also fix the inaccurate comment in listExportPendingComponentsIds: that
function runs in a workspace context (it reads bitMap), so the relevant
producer for hidden entries hitting that branch is workspace
cascade-on-snap or a fetch from a remote that ran the bare-scope
producer — not the bare-scope path itself, which never executes that
function.
…include hidden entries

listExportPendingComponents{,Ids} now always returns hidden lane entries
(skipWorkspace: true) alongside visible ones. The flag existed to give
'bit status' a workspace-only view for its 'staged components' section,
but every other caller (export, reset, status's pendingUpdateDependents
section) opted in already, and 'no lane' callers (export-from-main,
create-lane) didn't care. The function's job is now uniform: return
every locally-changed pending-export entry.

bit status partitions the result at the call site into stagedComponents
(visible) and pendingUpdateDependents (hidden). One call instead of two,
and the workspace-vs-lane split now lives only where it's needed.
…after export

Adds an e2e scenario documenting the local-side persistence of the
overrideUpdateDependents flag after a successful bit export. The flag
is cleared on the remote (sources.mergeLane) but stays `true` locally
because no hook in the export-success path clears it; only `bit reset`
does. The doc block on the scenario covers the downstream impact for
both directions (import guard blocks fetch updates; subsequent push
silently overwrites concurrent producer's hidden entries) and points
to the right structural fix (route hidden entries through
mergeLaneComponent's divergence check).
…ents

`fetchLanesUsingScope` (the bare-scope `bit fetch <scope>/<lane> --lanes`
path) and `getBitIdsForLanes` (the workspace objectsOnly fetch path) were
calling `lane.toComponentIds()` after that method was made visible-only.
Result: hidden `skipWorkspace: true` lane entries' Version objects were
never pulled to the local scope, even though the lane object itself
referenced their heads. Anything that subsequently tried to load those
versions (snapFromScope's internal import, merge engine, getDivergeData)
blew up with `expect <id> to have a Version object of "<hash>"`.

Switch both call sites to `toComponentIdsIncludeUpdateDependents()`. The
sibling `fetchLaneComponents` already does this correctly (with a comment
calling out exactly this requirement) — these were missed in the unify-
hidden-entries refactor.

Adds e2e scenario 17 exercising the bare-scope producer fetch path end-
to-end: workspace cascade-snap+export, then a fresh bare scope runs
`bit fetch --lanes` and `snapFromScope` to push a competing hidden
update. Without this fix the producer setup fails at the import step.
The scenario doubles as a known-leak demo for the override-flag-blocks-
fetch behavior covered in scenario 16's doc block.
…erge check

sources.mergeLane previously routed hidden (skipWorkspace) lane entries
through a separate override-flag-governed replacement path: 'remote is
authoritative on import unless local has overrideUpdateDependents=true;
client wins on export when override=true.' That winner-takes-all
semantic could silently overwrite a concurrent producer's hidden
cascade and required keeping the override flag in lock-step across
push/import boundaries — with subtle leaks like the local flag
persisting after export and blocking subsequent fetches from picking
up remote updates.

Drop the visible-only filter in the per-component loop and run every
lane component (visible + hidden) through mergeLaneComponent. Hidden
entries now get the same diverge guarantees as visible: same-head
no-op, target-ahead fast-forward, local-ahead no-op, divergent
push → ComponentNeedsUpdate (resolve via reset+re-cascade), divergent
import → silent-keep-local. The override flag is still set/cleared by
existing producers and serialized on the wire for backwards
compatibility with older clients, but no merge-path code reads it; it
becomes dead state to be removed in a follow-up.

Also: in the !existingComponent + isExport branch of mergeLaneComponent,
removeComponentFromUpdateDependentsIfExist must only run for visible
incoming entries. With hidden entries flowing through this branch, the
unconditional call would strip the just-added hidden entry on every
seed cascade.

Replaces the previous override-flag leak demos (old scenarios 16, 17)
with a single scenario showing the new behavior end-to-end: producer
pushes a hidden cascade, workspace fetches, workspace's local lane
fast-forwards to the producer's hash. The scenarios that previously
encoded the leaky semantics no longer have a reason to exist.
snapFromScope used to flip skipAutoTag to false whenever updateDependents
was set, working around external callers that hard-coded skipAutoTag: true
regardless of intent. The result was a hidden invariant in bit-core:
'we know better than the caller — autotag must run for cascade even though
you said skip it.'

Drop the override and respect the caller's input. Callers that produce
hidden updateDependents must pass skipAutoTag: false (or omit it) so the
reverse cascade in getLaneAutoTagIdsFromScope can find lane.components
that depend on the new entry and re-snap them.
After unifying hidden updateDependents into mergeLaneComponent's diverge
check, the override flag is no longer read anywhere in the merge path.
Every remaining set/clear and the schema field were dead bookkeeping.

Removed:
- Lane.overrideUpdateDependents field, getter, setter, wire-format
  serialization, LaneProps entry, clone propagation.
- model-component setting the flag on hidden-entry write.
- snapping.reset bookkeeping that scanned hidden entries to decide
  whether to clear the flag.
- sources.mergeLane post-export clear.
- e2e assertions on overrideUpdateDependents (scenarios 8, 9).
…vers

The flag is no longer read by any merge-path code in this codebase
(replaced by the unified diverge check in mergeLaneComponent). It's
restored here purely as a wire-format compat shim so that this
client's cascade pushes still propagate to remote scopes that haven't
yet upgraded to the new merge logic — older servers gate their
hidden-update branch on this flag being true.

All re-introduced surface is marked @deprecated with a one-line note
pointing at this rationale. After the rollout window closes (every
relevant server is on the unified merge path), drop:
- LaneProps.overrideUpdateDependents
- Lane.overrideUpdateDependents private field + setter
- toObject / Lane.from serialization
- clone propagation
- model-component.ts setter call on hidden-entry write
…pute hidden id set

- status.main.runtime: precompute a Set of hidden lane ids once, then a single-pass split
  of allPendingForExport into staged vs pendingUpdateDependents — was O(N·M) via repeated
  Lane.getComponent linear scans.
- status-formatter: include pendingUpdateDependentsOutput in joinSections so the new
  section actually appears in 'bit status' textual output (was constructed but unused
  in the main statusMsg, only surfaced in the structured 'sections' output).
Replace snapHiddenForMerge (a third snap path that bypassed makeVersion)
with snapping.snapForMerge — visible workspace comps and hidden lane
updateDependents now ride one makeVersion batch. Hidden snaps get fresh
log/buildStatus/flattenedDependencies/lane-history/stagedSnaps that the
old shortcut path skipped.
tagData.isNew is never truthy at this site — snapFromScope drops isNew
when building tagDataPerComp, parseVersionsFile hardcodes false, and a
genuinely new component has no parents for removeAllParents to remove
anyway.
bit lane checkout/revert are workspace-navigation operations and don't
mutate the lane object for visible components. Mutating it for hidden
updateDependents was an asymmetric exception — destructive lane edit
hidden inside a navigation command. If the user keeps working on the
lane, the next snap re-cascades; if they fork, the new lane starts fresh.
Neither path needs the historical hidden bucket pre-applied.
getAutoTagInfo loads [potentialComponents, ...changedComponents] from
the workspace, so a hidden updateDependent in changedComponents throws
MissingBitMapComponent. Filter those out — hidden cascade is already
covered by the scope-side getLaneAutoTagIdsFromScope pass.

Surfaced via scenario 13 (bit lane merge main): the merge engine queued
the hidden comp2 cascade snap, snapForMerge handed it to makeVersion,
and getAutoTagData blew up before any snap was created.
… present

If updateDependents is undefined but the override flag is true, an older
server could read the payload as 'authoritatively clear updateDependents'
and wipe its remote hidden bucket. Gate the wire-format compat shim on
the hidden bucket actually being non-empty so it stays one-shot.

Also fix scenario 8 comment to describe the actual reset mechanism
(batchId-based lane-history rewind), not the non-existent
Lane.updateDependentsBeforeCascade property.
…orkspace unification

Reverts the storage decision from #10331 (folding hidden updateDependents into
`lane.components` with `skipWorkspace: true`) while preserving every behavioral
fix from that PR.

Lane model goes back to two named fields: `lane.components` (visible) and
`lane.updateDependents` (hidden cascade entries). `toComponentIds()` once again
means "all of `lane.components`" with no surprising filter, and direct
iteration of `lane.components` no longer leaks hidden entries into external
APIs (auto-fixes the GraphQL leak in `getLanesData`).

Consumers that need to act on hidden entries now check `lane.updateDependents`
explicitly, so the call sites that branch on bucket are visible at a grep:
- merge-status-provider: 3-way merge baseline includes updateDependents
- model-component addVersion: bucket choice via existing entry, not a flag
- sources.ts mergeLane: per-component diverge for hidden via dual iteration
- snapping.reset / status / api-for-ide: explicit updateDependents lookups
- remote-lanes.syncWithLaneObject: caches updateDependents heads too

The remote-lanes addition is load-bearing — without it, hidden cascade reset
diverges against main's remote head and wipes the seeded entry instead of
rewinding to it.

Net: 280 insertions, 292 deletions. lane.ts shrinks from 478 to 395 lines
(-83). All 16 cascade e2e scenarios in update-dependents-cascade.e2e.ts pass.
Copilot AI review requested due to automatic review settings May 8, 2026 18:08
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR keeps lane.updateDependents as a separate “hidden” field (instead of unifying it into lane.components) while preserving the cascade/merge/export behavioral fixes introduced in the alternative approach. It updates multiple consumers to explicitly include hidden lane entries where needed (merge/diverge baselines, export pending detection, reset/head rewind, status surfacing) and adds an e2e suite to lock down the cascade scenarios.

Changes:

  • Extend lane/component flows (merge, snap, reset, fetch/import, version history) to treat lane.updateDependents as part of the lane graph without leaking hidden entries into workspace-facing behavior.
  • Improve UX/API surfacing by separating “exported updates” / pendingUpdateDependents from regular staged components.
  • Add a comprehensive e2e suite for updateDependents cascade behavior plus a helper to seed snapFromScope scenarios.

Reviewed changes

Copilot reviewed 27 out of 27 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
scopes/scope/version-history/version-history.main.runtime.ts Include hidden updateDependents when enumerating lane heads for version history.
scopes/scope/objects/models/model-component.ts Preserve hidden/visible bucket on addVersion; set lane-local head including updateDependents.
scopes/scope/objects/models/lane.ts Maintain separate updateDependents field; equality/clone updates; helper to include hidden heads.
scopes/scope/objects/models/lane-history.ts Persist updateDependents in lane history entries (separate from visible components).
scopes/scope/importer/importer.main.runtime.ts Always fetch lane objects for both visible + hidden entries to support merge/diverge.
scopes/scope/importer/import-components.ts Include updateDependents ids in fetch/import lane object paths.
scopes/scope/export/export.main.runtime.ts Exclude updateDependents from bitmap “not tracked” warnings; ensure lane-aware head population for divergence/export set.
scopes/scope/export/export-cmd.ts Split export output into “exported components” vs “exported updates” based on lane.updateDependents.
scopes/lanes/merge-lanes/merge-lanes.main.runtime.ts Default workspace merge to include updateDependents and prefetch main objects accordingly.
scopes/lanes/lanes/switch-lanes.ts Use toComponentIds() instead of direct lane.components mapping (visible-only semantics).
scopes/lanes/lanes/lane.cmd.ts Render lane history with optional updateDependents block.
scopes/component/status/status.main.runtime.ts Add pendingUpdateDependents to status output and split hidden vs visible pending-export entries.
scopes/component/status/status-formatter.ts Add formatted “pending update-dependents” section with collapsible summaries.
scopes/component/status/status-cmd.ts Include pendingUpdateDependents in JSON output.
scopes/component/snapping/version-maker.ts Ensure hidden entries don’t touch bitmap; include hidden entries in autotag/scope graph; stage hidden snaps for export.
scopes/component/snapping/snapping.main.runtime.ts Include auto-tagged ids in push set; add snapForMerge() pipeline for workspace merges including hidden entries.
scopes/component/snapping/reset-component.ts Populate lane-aware heads before diverge calculations to correctly reset hidden entries.
scopes/component/merging/merging.main.runtime.ts Avoid writing hidden entries to workspace; preserve bucket when updating lane; route merge snaps through snapForMerge().
scopes/component/merging/merge-status-provider.ts Include updateDependents as valid lane baselines for 3-way merge calculations.
components/legacy/scope/repositories/sources.ts Apply per-component diverge merge to both visible + updateDependents; fix reset to walk lane head chain for hidden entries.
components/legacy/scope/lanes/remote-lanes.ts Cache remote heads for updateDependents for correct diverge/reset behavior.
components/legacy/component-list/components-list.ts Ensure export-pending detection works for scope-only hidden updateDependents entries.
e2e/harmony/lanes/update-dependents-cascade.e2e.ts Add end-to-end coverage for cascade snap/merge/reset/fetch behaviors with hidden lane entries.
components/legacy/e2e-helper/snap-from-scope-runner.ts Add a subprocess runner intended to invoke SnappingMain.snapFromScope.
components/legacy/e2e-helper/e2e-snapping-helper.ts Add helper wrapper to spawn the runner for seeding updateDependents in e2e.
components/legacy/e2e-helper/index.ts Export the new snapping helper.
components/legacy/e2e-helper/e2e-helper.ts Wire the new snapping helper into the main e2e Helper.

Comment thread components/legacy/e2e-helper/e2e-snapping-helper.ts
davidfirst added 2 commits May 8, 2026 14:18
…pendent helper

Add Lane.findUpdateDependent / getUpdateDependentAsLaneComponent helpers and
route the 9 duplicated find calls + 4 LaneComponent synthesis sites through
them. Combine the two sequential pMap passes in sources.mergeLane and the two
Promise.all calls in remote-lanes.syncWithLaneObject. Drop the dead
shouldOverrideUpdateDependents() getter (wire-format-only field, zero readers)
and trim narrative WHAT-comments while keeping load-bearing WHY comments.

Net -57 lines across 12 files, no behavior change. All 43 cascade e2e
scenarios pass.
Copilot AI review requested due to automatic review settings May 8, 2026 21:11
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 27 out of 27 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

scopes/scope/objects/models/lane.ts:232

  • The docstring for setOverrideUpdateDependents() says the flag is "discarded before the lane is persisted", but Lane.toObject() serializes overrideUpdateDependents and Lane.parse() restores it, and model-component.addVersion() sets this flag for hidden lane writes (not just temp lanes). Please either adjust the comment to reflect the actual persistence/usage, or ensure the flag is cleared before persisting/exporting if it truly must remain temp-only.
  /**
   * wire-format compat shim only — older remotes gate their export-merge hidden-update branch on
   * this flag. Local code never reads it; setting it is safe only on a temp lane (e.g. `bit _snap`)
   * since it's discarded before the lane is persisted to the user's scope.
   */
  setOverrideUpdateDependents(overrideUpdateDependents: boolean) {
    this.overrideUpdateDependents = overrideUpdateDependents;
    this.hasChanged = true;

Comment thread components/legacy/e2e-helper/e2e-snapping-helper.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants