# Dependency visibility: public, private and friends {#root}

@status:doc/work @audience:user,author

[p01] A 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.

[p02]
```sh
vibe why org.vibevm.world/wal --path hello-vibe
```

```output
org.vibevm.world/wal — present: root-edge via hello-vibe -> org.vibevm.world/wal (override: none)
```

## Three marks on a requirement {#three-marks}

[p03] Every line under `[requires.packages]` is an edge from your [package](../glossary/index.xml#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.

> [p04] Each `[requires.packages]` edge gains an optional `access` property with three values — the provider-side seepage mark on the edge `P → Q`, declared by `P` about its own dependency `Q`:
>
> <spec://org.vibevm.core/vibevm/common/PROP-050#ACCESS-LEVELS>

[p05]
```toml
[requires.packages]
"org.example/style" = "^1.0"
"org.example/wal" = { version = "^2.0", access = "private" }
"org.example/inner" = { version = "^1.0", access = "friends-only" }
```

[p06] `public` 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.

> [p07] `access = "public"` — **the default.** `Q` seeps through `P` to *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.
>
> <spec://org.vibevm.core/vibevm/common/PROP-050#ACCESS-PUBLIC>

[p08] `private` 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.

> [p09] `access = "private"` — `Q` does not seep through `P` at all: the edge is traversed only when `P` itself 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.
>
> <spec://org.vibevm.core/vibevm/common/PROP-050#ACCESS-PRIVATE>

[p10] `friends-only` sits between the two. The dependency reaches only the consumers whose [friend closure](../glossary/index.xml#friend-closure) contains your package: those who deliberately named you a friend, directly or through a friend of theirs.

> [p11] `access = "friends-only"` — `Q` seeps through `P` only into consumers whose friend closure contains `P` (§2.4): those who deliberately named `P` (directly or transitively) a friend. The curated middle.
>
> <spec://org.vibevm.core/vibevm/common/PROP-050#ACCESS-FRIENDS>

[p12] Because 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](../glossary/index.xml#lock-file) makes that bill visible.

> [p13] **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** — mark `private` every edge that is not part of your consumable substance (WAL-class disciplines, dev tooling, heavy optional companions), and `friends-only` what 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.
>
> <spec://org.vibevm.core/vibevm/common/PROP-050#REEXPORT-USAGE-NORM>

## Friendship: opting in {#friendship}

[p14] Friendship 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.

> [p15] **Per-edge `friend = true|false` (default `false` — 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 = true` is the explicit opt-in; the original default-`true` belonged 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.
>
> <spec://org.vibevm.core/vibevm/common/PROP-050#FRIEND-EDGE-FLAG>

[p16] A `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.

> [p17] **The implication — owner-ratified (F10, 2026-08-23): a `friends-only` mark implies `friend = true` on its own edge.** Vouching for a package as part of your substance while standing in no relation to it is incoherent — and under the strict `friend = false` default, a `friends-only` chain would otherwise need *two* marks per hop, where forgetting the second silently kills the chain. With the implication, the owner's `A → B → C → D` chain works from one mark per hop, exactly as originally intended; an explicit `friend = false` beside a `friends-only` mark 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.
>
> <spec://org.vibevm.core/vibevm/common/PROP-050#FRIENDS-ONLY-IMPLIES-FRIEND>

[p18] A friend declares nothing about what it receives. When you befriend a package, everything it vouches for arrives without a line in your [manifest](../glossary/index.xml#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».

> [p19] **A friend declares nothing about the vouched content — by design.** When `R` befriends `P` (one explicit line: `friend = true` on its `P` edge, or a `friends` entry), everything `P` vouches for arrives with no mention in `R`'s manifest: `P`'s friends-only edge admits `Q` (rule (3)), the `grow` rule puts `Q` into `C(R)`, and `Q`'s own friends-only edges then open too — recursively, the owner's original transitivity requirement. Version choice for `Q` stays with the *voucher* (`P`'s constraint on its own edge) — the vouched set is a bundle `P` tested, not a menu `R` assembles. This is the standard re-export semantics of JPMS `requires transitive` (implied readability chains), Bazel `deps` + `exports*`, and Gradle `api` — a dependency on `P` is a dependency on «`P` with everything `P` stands on». Control never leaves the payer: the grant is explicit (the `false` default), `unfriend`/per-edge `exclude` prune point-wise, and the lock-diff + `vibe why` make every transit arrival legible. The hygiene norm transposes from Bazel strict-deps: transit covers «`Q` is here because `P` stands on it»; the moment `R`'s *own* text starts building on `Q`, `R` declares its own direct edge.
>
> <spec://org.vibevm.core/vibevm/common/PROP-050#TRANSIT-WITHOUT-DECLARATION>

[p20] The 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.

> [p21] **`access` and `friend` compose 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 — `public` presence, no friendship — is plain delivery: «Q reaches everyone above me; I take none of its gated substance». Adding `friend = true` to 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 the `friends-only` mark 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 own `friends` line, which is the payer-decides invariant.
>
> <spec://org.vibevm.core/vibevm/common/PROP-050#PUBLIC-PLUS-FRIEND>

## The visibility section, unfriend and exclude {#the-visibility-section}

[p22] Whatever 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.

> [p23] **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]`.
>
> <spec://org.vibevm.core/vibevm/common/PROP-050#VISIBILITY-SECTION>

[p24]
```toml
[visibility]
friends = ["org.example/inner"]
unfriend = ["org.example/noisy"]
allow-friends = ["org.example/partner"]
```

[p25] `unfriend` 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.

> [p26] `unfriend = ["group/name", …]` (node-level) removes the named packages from the declaring node's `grants(…)` — 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 name **`unfriend`** is owner-ratified (F2, 2026-08-23); the property list's `enemy` is retired.
>
> <spec://org.vibevm.core/vibevm/common/PROP-050#UNFRIEND-IS-NODE-SCOPED>

[p27] `exclude`, 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.

> [p28] `exclude = ["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 in `E(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-only `override`, which may also re-house this syntax (the semantics here stand either way).
>
> <spec://org.vibevm.core/vibevm/common/PROP-050#EXCLUDE-IS-EDGE-SCOPED>

[p29] A 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.

> [p30] **Owner-ordered design (F8, 2026-08-23: «спроектируй механизм allow-friends… похоже на возможность построить exhaustive замыкание sealed classes»).** A provider `G` may declare, node-level, who is permitted to *enter friendship with it* — the Java-`sealed`/`permits` shape transplanted onto the friendship relation (design ratified — «годится»):
>
> <spec://org.vibevm.core/vibevm/common/PROP-050#ALLOW-FRIENDS-DESIGN>

> [p31] 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.
>
> <spec://org.vibevm.core/vibevm/common/PROP-050#ALLOW-FRIENDS-STATES>

> [p32] The check sits on the **grant giver**: a grant `N → G` (an explicit `friend = true` edge, a `friends` entry, or an F10-implied one) participates in any closure only if `allow-friends(G)` is absent or covers `N`. A friends-only re-export hop `F → G` in `C(R)` likewise requires `F` permitted by `G`. A rejected grant is a **warning, never an error** (the §5 unknown-target precedent) — the closure simply does not grow there.
>
> <spec://org.vibevm.core/vibevm/common/PROP-050#ALLOW-FRIENDS-CHECKPOINT>

[p33] A 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.

> [p34] **The seal gates friendship, never delivery (pinned at the W6 landing).** A sealed `G` still *arrives* wherever a declarant's `friends-only` edge delivers it — the declarant owns its own edge and could as well have marked it `public`; what the seal closes is **entry into `G`'s circle**: a rejected grant keeps `G` out of every closure, so `G`'s own friends-only inner content stays shut and the grant surfaces as a `RejectedGrant` warning. The observable difference between sealed and unsealed is always the inner content and the diagnostics, never the presence of `G` itself. Proven end-to-end by `cli_visibility_power.rs` (unseal and exact-circle scenarios).
>
> <spec://org.vibevm.core/vibevm/common/PROP-050#SEAL-GATES-FRIENDSHIP-NOT-DELIVERY>

## Repairing an edge you do not own {#overrides}

[p35] Sometimes 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`.

> [p36] **Owner-ruled (2026-08-23): `override` is lawful in any manifest, not only the root** («разрешён не только в корневом манифесте, а где угодно»). Any node `N` may carry an `[override]` table whose entries rewrite *foreign* edges — their `access`, `friend`, presence (`exclude = true`), or a target's `allow-friends` — and the rewrite acts wherever `N` stands on the chain: an aggregator repairs or reshapes a member's edge for **all of its own consumers**, exactly as it curates its delivery with `exclude`. 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).
>
> <spec://org.vibevm.core/vibevm/common/PROP-050#OVERRIDE-ANYWHERE>

[p37]
```toml
[override]
"org.example/member -> org.example/wal" = { access = "public" }
"org.example/partner" = { allow-friends = "*" }
```

[p38] An edge key names the two ends of the edge with an arrow; a package key names one package and rewrites its `allow-friends`. [Overrides](../glossary/index.xml#override) 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.

> [p39] **Path-stack semantics.** An override applies to chains that pass through its declarant: walking a chain `R → … → N → … → P → Q`, the effective attributes of the edge `P → Q` are 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 to `R`) wins on the chains it participates in. Effective attributes are therefore per-chain; `E(R)` and the `grow` rule quantify **existentially over chains** (a package is present / a hop extends the closure if *some* chain admits it), which is the diamond behaviour `exclude` already 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).
>
> <spec://org.vibevm.core/vibevm/common/PROP-050#OVERRIDE-PATH-SEMANTICS>

[p40] The same key, `override`, also serves the older array form `[[override]]` that pins a replacement source for one [coordinate](../glossary/index.xml#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.

> [p41] **Syntax note (W1 landing):** the manifest already carried `[[override]]` — the array-of-tables registry-pin form (`OverrideSection`). The visibility table lives under the same `override` key 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.
>
> <spec://org.vibevm.core/vibevm/common/PROP-050#OVERRIDE-KEY-COEXISTENCE>

## What arrives in your tree {#what-arrives}

[p42] The packages that the marks admit from your root form the [effective set](../glossary/index.xml#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.

> [p43] Version resolution operates on `E(R)` only: private edges of non-root packages contribute no constraints, fetch nothing, and cannot conflict. `vibe.lock` records `E(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.
>
> <spec://org.vibevm.core/vibevm/common/PROP-050#RESOLVE-EFFECTIVE-ONLY>

[p44] The 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](../glossary/index.xml#boot-lane).

> [p45] `vibedeps/` holds exactly `E(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 declared `private` (or `friends-only`) by whatever requires it simply never arrives in a consumer's tree — no snippet, no `vibevm/vibespecs/WAL.xml` scaffold, no INDEX row.
>
> <spec://org.vibevm.core/vibevm/common/PROP-050#MATERIALISE-EFFECTIVE-ONLY>

[p46] Your 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.

> [p47] Rule (1) of §2.5 — the root's own edges always traverse — combined with explicit `private` marks 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 by `access`** rather 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.
>
> <spec://org.vibevm.core/vibevm/common/PROP-050#PRIVATE-IS-THE-DEV-WORLD>

[p48] Because 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.

> [p49] **Closure-drift visibility.** The lock carries `E(R)` with each member's lane cost (bytes/tokens of its contribution); `vibe update` prints 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.
>
> <spec://org.vibevm.core/vibevm/common/PROP-050#VERIFY-LOCK-DIFF>

## Asking why {#asking-why}

[p50] `vibe 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.

> [p51] **Observability: `vibe why <group>/<name>`.** For any package, print the chains that admit it into `E(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 tree` gains the same annotations.
>
> <spec://org.vibevm.core/vibevm/common/PROP-050#VIBE-WHY>

## Edge cases and rules {#edge-cases}

[p52] A 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.

