Keep lane.updateDependents as a separate field (alternative to #10331)#10358
Open
davidfirst wants to merge 55 commits intomasterfrom
Open
Keep lane.updateDependents as a separate field (alternative to #10331)#10358davidfirst wants to merge 55 commits intomasterfrom
davidfirst wants to merge 55 commits intomasterfrom
Conversation
…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.
…dependents-skipworkspace
…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.
…dingComponentsIds
… 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.
…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.
Contributor
There was a problem hiding this comment.
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.updateDependentsas part of the lane graph without leaking hidden entries into workspace-facing behavior. - Improve UX/API surfacing by separating “exported updates” /
pendingUpdateDependentsfrom regular staged components. - Add a comprehensive e2e suite for updateDependents cascade behavior plus a helper to seed
snapFromScopescenarios.
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. |
…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.
Contributor
There was a problem hiding this comment.
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", butLane.toObject()serializesoverrideUpdateDependentsandLane.parse()restores it, andmodel-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;
…rom-scope runner expectations
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Alternative to #10331. Same behavioral fixes, simpler storage.
#10331 unifies
lane.updateDependentsintolane.componentsvia askipWorkspace: trueflag. This PR keeps the master shape —lane.components(visible) andlane.updateDependents(hidden cascade entries) — as two named fields, while preserving every behavioral improvement #10331 introduced (cascade snap routing throughmakeVersion, 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, butlane.toComponentIds()returns visible-only. Every direct iteration oflane.componentsbecomes a place that might need to filterskipWorkspace. We hit this concretely with the GraphQLLane.componentsresolver ingetLanesDataleaking hidden entries to external consumers.Two named fields make the bucket choice explicit at every call site, and
toComponentIds()once again means "all oflane.components" with no surprising filter.Net change
lane.tsshrinks from 478 → 395 lines (-83 in the model).update-dependents-cascade.e2e.tspass.Approach
The model-side cost of the unification (wire-format split in
toObject/Lane.parse, theset updateDependentscollision logic, theisEqualcomplexity, theaddComponentskipWorkspace bookkeeping) goes away. The cost moves to consumers that need to act on both buckets, which now checklane.updateDependentsexplicitly:merge-status-provider: include hidden as the 3-way merge baselinemodel-component.addVersion: route toaddComponentvsaddComponentToUpdateDependentsbased on the existing entry's bucketsources.ts mergeLane: per-component diverge applied to both buckets via a unifiedmergeLaneComponenttaking anisHiddenflagremoveComponentVersions(reset): rewind hidden entries viaaddComponentToUpdateDependentsinstead of mutating aLaneComponentin placestatus/api-for-ide/components-list: explicitlane.updateDependentslookupsremote-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: callspopulateLocalAndRemoteHeadssogetLocalHashessees the hidden cascade as the local headTest plan
npm run lint(typecheck + oxlint) — cleannpm run e2e-test --bit_bin=bit4 -- --grep "local snap cascades updateDependents on the lane"— 43/43 passRelated
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.