Dependency visibility: public, private and friends
01A package you depend on has dependencies of its own, and those can reach your project too. Whether they do is decided where each dependency is declared, one line at a time: everyone gets it, nobody gets it, or only the projects that have named the declaring package a friend. This page explains the three marks, the friendship that opens the middle one, what ends up in your tree as a result, and how to ask vibe why a package is there or missing.
vibe why org.vibevm.world/wal --path hello-vibe
org.vibevm.world/wal — present: root-edge via hello-vibe -> org.vibevm.world/wal (override: none)
Three marks on a requirement
03Every line under [requires.packages] is an edge from your package to the one it names, and the edge carries an optional access mark. The mark is the declaring package's word about its own dependency: how far it may seep upward, to the projects that depend on the declaring package.
04 Each[requires.packages]edge gains an optionalaccessproperty with three values — the provider-side seepage mark on the edgeP → Q, declared byPabout its own dependencyQ:
05[requires.packages]
"org.example/style" = "^1.0"
"org.example/wal" = { version = "^2.0", access = "private" }
"org.example/inner" = { version = "^1.0", access = "friends-only" }
06public is the default and needs no writing: the dependency reaches every consumer above you, however far up, with nothing to opt into. It is the right mark for everything your own text builds on, and for the members of a collection.
07access = "public"— the default.Qseeps throughPto every consumer above, transitively along the whole hierarchy, with no opt-in. For edges whose target is part of the declarant's substance for all consumers — a collection's members, a stack's core, and every ordinary «my text builds on this» edge.
08private stops the dependency at your door. The edge is followed only when your package is itself the root of the installation, which is the case in your own checkout. It is the mark for tooling and for the disciplines that shape how you work rather than what you ship.
09access = "private"—Qdoes not seep throughPat all: the edge is traversed only whenPitself is the consumer root (§4.4 — the dev world). An implementation detail in the strictest sense — the explicit mark for WAL-class disciplines and dev tooling.
10friends-only sits between the two. The dependency reaches only the consumers whose friend closure contains your package: those who deliberately named you a friend, directly or through a friend of theirs.
11access = "friends-only"—Qseeps throughPonly into consumers whose friend closure containsP(§2.4): those who deliberately namedP(directly or transitively) a friend. The curated middle.
12Because presence flows by default, the author's duty is to narrow on purpose. Mark private every edge that is not part of what you offer, and friends-only what only your inner circle should build on. Every public edge costs your consumers reading budget, and the lock file makes that bill visible.
13 Authoring norm for narrowing marks (the JPMS community rule, transposed and then inverted by the public default): since presence flows by default, the author's duty is to narrow deliberately — markprivateevery edge that is not part of your consumable substance (WAL-class disciplines, dev tooling, heavy optional companions), andfriends-onlywhat only your inner circle should build on. Aggregator-style «one edge pulls the world» is lawful for collections, whose members are their declared substance (§4.6); everyone else answers for every public edge with its lane cost (§7 measurements make the bill legible). Advisory, policed by the §7 lints, enforced by nobody — strict-deps culture with the autofix command in place of ceremony.
Friendship: opting in
14Friendship is declared by the consumer, never assumed. On an edge, friend = true says «I enter friendship with this package»; in the [visibility] section, friends = ["org.example/inner"] says the same for a package you do not require directly. The default is false: an ordinary edge takes the package, not its inner circle. Presence is generous and friendship is stingy, and the two defaults differ on purpose.
15 Per-edgefriend = true|false(defaultfalse— owner re-ruled 2026-08-23). Friendship is a raised privilege — receiving a target's gated substance — and raised privileges are not granted where they may never be used: an ordinary edge takes the package, not its inner circle.friend = trueis the explicit opt-in; the original default-truebelonged to the earlier one-field paradigm and is retired with it. With the public presence default (§2.2) the two defaults form the deliberate asymmetry: presence is generous, friendship is stingy — the ordinary world works with zero ceremony, the curated world opens only by explicit word.
16A friends-only mark on your own edge implies friendship with the package it names, so a chain of friends works from one mark per hop. Write friend = false beside it when you want to deliver a package to your circle without entering its circle yourself.
17 The implication — owner-ratified (F10, 2026-08-23): afriends-onlymark impliesfriend = trueon its own edge. Vouching for a package as part of your substance while standing in no relation to it is incoherent — and under the strictfriend = falsedefault, afriends-onlychain would otherwise need two marks per hop, where forgetting the second silently kills the chain. With the implication, the owner'sA → B → C → Dchain works from one mark per hop, exactly as originally intended; an explicitfriend = falsebeside afriends-onlymark overrides the implication and yields the lawful no-vouch cell of ##ACCESS-FRIEND-MATRIX (terminal delivery to the circle) — no lint, it is a real intent.
18A friend declares nothing about what it receives. When you befriend a package, everything it vouches for arrives without a line in your manifest, at the versions the voucher chose, and the friends-only edges of those packages open in turn. The moment your own text starts building on one of them, declare your own direct edge: transit says «here because a friend stands on it», not «mine».
19 A friend declares nothing about the vouched content — by design. WhenRbefriendsP(one explicit line:friend = trueon itsPedge, or afriendsentry), everythingPvouches for arrives with no mention inR's manifest:P's friends-only edge admitsQ(rule (3)), thegrowrule putsQintoC(R), andQ's own friends-only edges then open too — recursively, the owner's original transitivity requirement. Version choice forQstays with the voucher (P's constraint on its own edge) — the vouched set is a bundlePtested, not a menuRassembles. This is the standard re-export semantics of JPMSrequires transitive(implied readability chains), Bazeldeps+exports*, and Gradleapi— a dependency onPis a dependency on «Pwith everythingPstands on». Control never leaves the payer: the grant is explicit (thefalsedefault),unfriend/per-edgeexcludeprune point-wise, and the lock-diff +vibe whymake every transit arrival legible. The hygiene norm transposes from Bazel strict-deps: transit covers «Qis here becausePstands on it»; the momentR's own text starts building onQ,Rdeclares its own direct edge.
20The two marks are independent and point in opposite directions, so they compose. A public edge with friend = true delivers the package to everyone above you and opens its friends-only doors for you alone; that friendship does not travel onward. Passing friendship on is exactly what the friends-only mark does.
21accessandfriendcompose independently on one edge (owner-confirmed 2026-08-23: «access и friend разные понятия»). The two marks point in opposite directions, so their product is well-defined. The default shape —publicpresence, no friendship — is plain delivery: «Q reaches everyone above me; I take none of its gated substance». Addingfriend = trueto a public edge reads: «and for myself I enter friendship with Q, so Q's friends-only doors are open in my own perspective». That friendship does not travel onward through a public edge — re-exporting friendship is exactly what thefriends-onlymark does (##PUBLIC-GIVES-PRESENCE-NOT-FRIENDSHIP) — so in another root's closure a public edge contributes presence and nothing more. The remaining corner — «deliver publicly and vouch friendship onward» in a single edge — is deliberately inexpressible: a consumer that wants Q's gated substance opts in with its ownfriendsline, which is the payer-decides invariant.
The visibility section, unfriend and exclude
22Whatever concerns the package as a whole rather than one edge lives in one section, [visibility], the same for a project and for a package: friends, unfriend, allow-friends and ignore-concept-warnings. Beside it stands the top-level [override] table described below.
23 The node-level vocabulary lives in one role-blind[visibility]section —friends,unfriend,allow-friends(§2.8),ignore-concept-warnings(##CONCEPTS-GATE-SOFTENED) — plus the sibling top-level[override]table (§2.9). One section serves both manifest roles by construction (PROP-024 equipotence): no field is duplicated between[project]and[package].
24[visibility]
friends = ["org.example/inner"]
unfriend = ["org.example/noisy"]
allow-friends = ["org.example/partner"]
25unfriend removes the named packages from the friendships you pass on. They still arrive at your level when an edge admits them, but nobody sees them as your friends through you. Another package in the same tree may still befriend them; the pruning is yours alone.
26unfriend = ["group/name", …](node-level) removes the named packages from the declaring node'sgrants(…)— and therefore from every friend closure as seen through that node. The unfriended package «притянется, но будет явно исключён из цепочки транзитивности внутренних друзей»: still usable at the declaring level (its edge, if any, still traversable by its own access), just never re-exported as a friend through the declarant. Node-scoped by owner law: «они выбрасываются из замыкания ТОЛЬКО с точки зрения той ноды, которая объявила их unfriend — а какой-нибудь другой пакет в иерархии может нормально включить их в замыкание» — another node's friends-only chain delivers the same package untouched. The nameunfriendis owner-ratified (F2, 2026-08-23); the property list'senemyis retired.
27exclude, written on an edge, is the other prune: the named packages are cut from every chain that passes through that edge, however deep, even where they are public. A different path may still deliver them, and then they are present through that path. Exclusion can only narrow what the declaring packages granted, never widen it, so any package may declare it.
28exclude = ["group/name", …](per-edge) kills the named packages in every chain passing through the declaring edge — «исключены из цепочки транзитивных подключений вообще, даже если внутри они объявлены как public». Maven-exclusions semantics, owner-ratified (F4, 2026-08-23: «exclude per-ребро Maven-style. Можно per-ребро делать глубокие эксклюды по иерархии — это не глобальный deny-list, это сужение в рамках поддерева»): the pruning reaches arbitrarily deep, but only within this edge's subtree; a different path still delivers the package, and then it simply exists inE(R)via that path — classic diamond behaviour, no global veto. Exclusion is pure subtraction — it can only narrow what providers granted, never widen — which is why any node may declare it as part of shaping its own delivery; the expansive counterpart is F9's root-onlyoverride, which may also re-house this syntax (the semantics here stand either way).
29A package can also say who may befriend it. allow-friends absent means anyone; an empty list seals the package, so its gated content exists only in its own checkout; a list names the exact circle. The check falls on the one who grants friendship: a grant the package does not permit is a warning, never an error, and the closure does not grow there.
30 Owner-ordered design (F8, 2026-08-23: «спроектируй механизм allow-friends… похоже на возможность построить exhaustive замыкание sealed classes»). A providerGmay declare, node-level, who is permitted to enter friendship with it — the Java-sealed/permitsshape transplanted onto the friendship relation (design ratified — «годится»):
31
Three states: field absent — friendship is open, anyone's grant works (the default, today's semantics); empty list — sealed: nobody enters, the gated substance exists only in G's own dev world; a list — exactly the named circle.
32 The check sits on the grant giver: a grantN → G(an explicitfriend = trueedge, afriendsentry, or an F10-implied one) participates in any closure only ifallow-friends(G)is absent or coversN. A friends-only re-export hopF → GinC(R)likewise requiresFpermitted byG. A rejected grant is a warning, never an error (the §5 unknown-target precedent) — the closure simply does not grow there.
33A seal gates friendship, not delivery. A sealed package still arrives wherever a friends-only edge delivers it; what stays shut is its own inner circle, and the rejected grant is reported. vibe friends org.example/partner prints the whole picture for one package: open, sealed or a named circle, who befriends it, which grants it rejects, and whether it sits in your closure.
34 The seal gates friendship, never delivery (pinned at the W6 landing). A sealedGstill arrives wherever a declarant'sfriends-onlyedge delivers it — the declarant owns its own edge and could as well have marked itpublic; what the seal closes is entry intoG's circle: a rejected grant keepsGout of every closure, soG's own friends-only inner content stays shut and the grant surfaces as aRejectedGrantwarning. The observable difference between sealed and unsealed is always the inner content and the diagnostics, never the presence ofGitself. Proven end-to-end bycli_visibility_power.rs(unseal and exact-circle scenarios).
Repairing an edge you do not own
35Sometimes the mark you need is on an edge you do not own: a member of a collection marked a dependency private that your consumers need, or a package sealed itself against you. The [override] table rewrites foreign edges, and it is lawful in any manifest, at the root or below it. An aggregator uses it to reshape a member's edge for all of its own consumers, the way it curates its delivery with exclude.
36 Owner-ruled (2026-08-23):overrideis lawful in any manifest, not only the root («разрешён не только в корневом манифесте, а где угодно»). Any nodeNmay carry an[override]table whose entries rewrite foreign edges — theiraccess,friend, presence (exclude = true), or a target'sallow-friends— and the rewrite acts whereverNstands on the chain: an aggregator repairs or reshapes a member's edge for all of its own consumers, exactly as it curates its delivery withexclude. The threat model follows the owner's earlier ruling: a deliberate break-in is not an attack (the developer can edit any file on disk anyway); this is the official verb that replaces reflection-style hacks — and it stays quiet (pull-based provenance only).
37[override]
"org.example/member -> org.example/wal" = { access = "public" }
"org.example/partner" = { allow-friends = "*" }
38An edge key names the two ends of the edge with an arrow; a package key names one package and rewrites its allow-friends. Overrides apply along the chains that pass through the manifest declaring them. The one nearer to the root is applied later and wins, so the root always has the final word, and an intermediary wins only on the chains it takes part in.
39 Path-stack semantics. An override applies to chains that pass through its declarant: walking a chainR → … → N → … → P → Q, the effective attributes of the edgeP → Qare its declared attributes masked by the[override]tables of the chain's nodes in order, nearer-to-root applied later and winning — the root can re-override any intermediary, the payer always has the final word; between intermediaries, the outer (closer toR) wins on the chains it participates in. Effective attributes are therefore per-chain;E(R)and thegrowrule quantify existentially over chains (a package is present / a hop extends the closure if some chain admits it), which is the diamond behaviourexcludealready has. Determinism is preserved — masks are static declarations, the graph is acyclic, and the implementation dedups identical mask-states while walking the DAG (override tables are rare, so the practical state count stays small).
40The same key, override, also serves the older array form [[override]] that pins a replacement source for one coordinate. The two shapes are told apart by their form, either alone is lawful, and one manifest carrying both is a loud error rather than a merge.
41 Syntax note (W1 landing): the manifest already carried[[override]]— the array-of-tables registry-pin form (OverrideSection). The visibility table lives under the sameoverridekey as an ordinary table; the wire layer distinguishes the two shapes structurally (array vs table), either form alone is lawful, and one manifest carrying both is a loud validation/serialisation error rather than a silent merge. A future wave may retire or rename the legacy form; until then the coexistence is deliberate.
What arrives in your tree
42The packages that the marks admit from your root form the effective set, and it is the only set vibe works with. Version resolution runs over it alone: a private edge of a package that is not your root contributes no constraint, fetches nothing and cannot conflict. The lock file records the effective set, so your lock never carries another package's tooling.
43 Version resolution operates onE(R)only: private edges of non-root packages contribute no constraints, fetch nothing, and cannot conflict.vibe.lockrecordsE(R)— the lock of a consumer no longer contains other packages' dev-world entries. Version unification (one node per(group, name), PROP-003/017) is unchanged within the effective set. A welcome simplification vs code ecosystems: the Cargo-RFC-1977 problem («may private deps duplicate at different versions?») does not arise — an invisible package has no copies at all.
44The dependency tree under vibedeps/ holds exactly that set. A package that is excluded or invisible leaves no folder, no cache entry for your world and no text in the boot lane.
45vibedeps/holds exactlyE(R): an excluded or invisible package leaves no slot, no cache entry for the root's world, no lane text. This is the structural fix for the WAL specimen: a wal flow declaredprivate(orfriends-only) by whatever requires it simply never arrives in a consumer's tree — no snippet, novibevm/vibespecs/WAL.xmlscaffold, no INDEX row.
46Your own edges are always followed, private ones included. That is how one [requires] is both your development set and your contract, split edge by edge rather than by section: in your checkout the private tooling materialises; consumed as a dependency, only the seeping edges do.
47 Rule (1) of §2.5 — the root's own edges always traverse — combined with explicitprivatemarks resolves the open tail of the equipotence wave (PROP-024): a package's[requires]is simultaneously its dev-set and its contract, split per-edge byaccessrather than by section. When the package is the consumer root (a dev checkout —[project]or[package], equipotently), all its edges traverse and its private tooling materialises; when it is consumed as a dependency, only its seeping edges do. No separate dev-dependencies section needed.
48Because a mark in the middle of the graph can widen what reaches you, vibe update prints the change to the effective set: which packages enter or leave and how much reading budget they add or remove. A widening is a reviewed event, not a quiet seep.
49 Closure-drift visibility. The lock carriesE(R)with each member's lane cost (bytes/tokens of its contribution);vibe updateprints the closure diff — packages entering/leaving and the lane-cost delta — so a mid-graph re-export widening (##CLOSURE-DRIFT-CONTROL) is a reviewed event, not a silent seep.
Asking why
50vibe why answers the question a visibility system otherwise turns into folklore. For a package that is present it prints the chain that admits it, each hop with its rule, its access mark and where the friendship came from. For a package that is absent it prints the nearest blocked chains and what blocked each one: a private edge, a missing friendship, an unfriend or an exclude. vibe tree carries the same annotations on every node.
51 Observability:vibe why <group>/<name>. For any package, print the chains that admit it intoE(R)— each hop annotated with its rule ((1)/(2)/(3)), access mark, and friendship provenance — and for an absent package, the nearest blocked chains and what blocked them (private edge / missing friendship / unfriend / exclude). The debugging surface without which a visibility system rots into folklore;vibe treegains the same annotations.
Edge cases and rules
52A package reached by two paths is present as soon as one path admits it; an exclude on the other path changes nothing. Excluding is subtraction and may be declared anywhere; widening is an override, and the root can always re-override an intermediary. Friendship, unfriending and sealing change which chains are open, never which versions are chosen: the version of a vouched package stays with the voucher.