PROP-050: Dependency visibility — access, friendship, and the friend closure
01Status: BUILT — 2026-08-23. The full build landed the same day the design closed: W1 engine + manifest vocabulary, W2 resolution/lock on E(R) (schema v6, provenance), W3 lane goldens, W4 gate softening, W5 observability (vibe why/vibe friends/closure diff/tree annotations/hygiene cell), W6 power-instrument proofs, W7 deliberate narrowing (fractality group; host lanes byte-identical), W8 close-out. Build record: vibevm/vibespecs/terraforms/VISIBILITY-BUILD-PLAN-v0.1.xml; measurements: vibevm/vibespecs/research/dependency-visibility-2026-08/04-measurements.xml; leftovers: BACKLOG B-102..B-105. All ten forks stand ruled (F1–F4, F6, F10 as drafted; F5 → no-legacy; F7 → warnings; F8 → designed+ratified; F9 → official, quiet, any-node); the only unruled sketch is ##CONCEPT-GATE-DIRECTION (B-105, non-blocking). Ratification history in §9/§10 stands as authored.
02Related: PROP-009 (the loading model and the link axis), PROP-034 / PROP-035 §12 / PROP-038 (the linker this proposal re-layers), PROP-048 (tokenomics — the law this system serves), PROP-049 (the snippet genre — §4.3 settles its fate under visibility), PROP-046 (consumer sovereignty — the same philosophy), PROP-024 (role equipotence — visibility is role-blind), PROP-028 (collections — the migration's largest client).
1. Motivation — seepage is a budget defect, not a secrecy defect
03Today the requires closure IS the effective set: every transitive dependency of every dependency reaches the consumer root, lands in INDEX.md (or, forced, in the static lane), and spends the root's context budget. The only controls are materialisation-side (link modes decide how a package loads, never whether it arrives) and the PROP-049 genre machinery (which polices textual presupposition, not structural arrival). The WAL leak — every new project sprouting vibevm/vibespecs/WAL.xml because a package chain delivered the wal flow — is the type specimen of the defect class.
04The inversion thesis. In every prior-art system (C++ friend, Java module exports, OSGi, Bazel visibility) visibility is provider-declared secrecy: the provider protects its invariants by naming who may see in. In vibevm the scarce good is not the provider's encapsulation but the consumer's context budget (PROP-048): a package that seeps uninvited costs tokens in every session of every downstream project. Visibility here is therefore budget control: the provider marks how far each edge seeps (access — presence flows along declared edges by default, §2.2, so loaders never silently lose what was always meant to be available), while the gated substance never arrives uninvited — friendship is an explicit act on both sides (friends-only mark ∧ explicit grant, §2.3–2.4) — and every arrival is measurable and refusable (vibe why, lock-diff, unfriend/exclude/override). This inverts the direction of every friendship mechanism in prior art — ours is consumer-declared opt-in, not provider-declared allowlisting — and the consequences of that inversion are traced through §5.
05Owner mandate (2026-08-23, verbatim core): «Каждая зависимость в vibe.toml должна обладать несколькими новыми опциональными свойствами… Пакеты с доступом public транзитивно просачиваются вдоль всей иерархии… потребительский корень проекта — это причина, почему дальше начинают происходить вычисления всей остальной иерархии пакетов… должна работать транзитивность внутренних друзей… Таким образом у нас получается некое общее "замыкание друзей"… Чтобы это замыкание не росло бесконтрольно, нужно предусмотреть параметры с исключениями».
2. The model
2.1 Perspectives — relations first, materialisation at the root
06Every visibility notion in this document is per-node: each manifest's declarations define how the graph looks from that node. An interior node's friendships, unfriendings and exclusions are «просто отношение между пакетами» — pure relations, materialising nothing.
07The consumer root is the cause of computation (owner law). Only when a node is the session's consumer root (PROP-024: [project] or [package], equipotently) do its perspective's relations resolve into an effective set, get version-resolved, materialised into vibedeps/, and compiled into lanes. «Потребительский корень проекта — это причина, почему дальше начинают происходить вычисления всей остальной иерархии пакетов.»
2.2 Edge access — how far this edge may seep
08Each [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:
09[requires.packages]
"flow:org.x/api-conventions" = { version = "^1.0" } # access = "public" — the default
"flow:org.x/wal" = { version = "^1.0", access = "private" } # dev-world only, seeps nowhere
"flow:org.x/internal-style" = { version = "^1.0", access = "friends-only" } # seeps to those who befriended P
- 10
access = "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. access = "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.access = "friends-only"—Qseeps throughPonly into consumers whose friend closure containsP(§2.4): those who deliberately namedP(directly or transitively) a friend. The curated middle.
11Default public — owner-ruled (2026-08-23), and the asymmetry of failures decides it. The owner: «public по умолчанию точно должен быть true, потому что бессмысленное ограничение доступа к скриптам может привести к тому, что у нас сломаются динамические и статические лоадеры (без разбора ограничили то, что подразумевалось всегда доступным)». The deeper argument this survives on: the two failure modes are not symmetric in observability. Over-delivery (a forgotten private) costs tokens — visible in vibe why, lane sizes, the lock-diff, and fixable by any consumer (exclude, unfriend, override). Under-delivery (a forgotten public under a private default) makes the agent silently dumber — a hole in loaded knowledge that no measurement surfaces and only the package's author can fix. A budget system must push errors toward the observable side; presence therefore defaults open, and the budget discipline lives in explicit narrowing marks, the §7 observability, and the §5 authoring norm — not in the default. This replaces the draft's default-private rationale (fail-closed presence), which the ruling retires.
2.3 Friendship — consumer-declared, two grant forms
12Friendship is declared by the consumer of content, never by its provider: naming a package your friend means you receive what it shares friends-only. There is no provider-side allowlist (§9 F8 keeps the door open). Two grant forms:
- 13Per-edge
friend = 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. - The implication — owner-ratified (F10, 2026-08-23): a
friends-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. - Node-level
friends = ["group/name", …]. Explicit befriending of any package — «можно указать и свои прямые зависимости, и вообще любые зависимости» — including ones the node does not require directly (e.g. a practice known to arrive deep in the closure whose friends-only companions the root wants).
14The 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].
15access 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.
16The full 3 × 2 matrix (owner-requested completeness check, 2026-08-23). For an edge P → Q, read across three observers — an arbitrary consumer above P; a friend of P's (a root R with P ∈ C(R)); and P itself as root (its dev world). friend answers one question everywhere — «does the observer get Q's own gated substance» — while access picks the audience that gets Q at all:
access |
friend |
consumer above P | friend of P | P as root | practical name |
|---|---|---|---|---|---|
public |
false (defaults) |
gets Q | gets Q | gets Q | plain delivery — the ordinary edge |
public |
true |
gets Q | gets Q | gets Q + Q's circle | delivery + personal friendship |
friends-only |
true |
— | gets Q + Q ∈ C(R): chain continues, Q's circle opens | gets Q + Q's circle | the chain link — transit with vouching (the F10 shape) |
friends-only |
false |
— | gets Q, chain stops: Q's circle stays shut | gets Q only | terminal delivery to the circle — share without vouching |
private |
false |
— | — | gets Q | dev tool |
private |
true |
— | — | gets Q + Q's circle | dev tool with deep integration |
- 18All six cells are distinct and each names a real intent — no redundancy, no gap. The one that looked dead —
friends-only+friend = false— turns out load-bearing: it separates delivery to the circle from vouching onward, which is exactly the distinction the two-parameter split exists to express (rule (3) grants presence from the mark alone; thegrowrule demands mark ∧ grant for the chain to continue). The parameter set is confirmed correct by exhaustion.
19A 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.
20What friends-only is for — the practical genres: (a) usage surface vs extension surface — a family (e.g. git-practices) delivers its practices public, while the author-side toolkit for writing a new member (templates, style, test matrices) rides friends-only: consumers consume, extenders befriend the family and receive the workshop; (b) platform inner layer — a platform ships its API text to all, its implementation-notes layer to befriended plugins (the Eclipse x-friends case, inverted to consumer opt-in, and sealable with §2.8 when curation is wanted); (c) incubation — an unstable companion practice circulates among befriended dogfood roots before its mark widens to public (a feature release per ##ACCESS-IS-SEMVER-SURFACE); (d) heavy opt-in tier — an advanced companion whose token weight should not tax every consumer, delivered only to those who ask; (e) the WAL shape — a redbook-like collection could carry a wal-like discipline on a friends-only edge: ordinary consumers never see it, while a root that wants the full discipline writes friends = ["…/redbook"] and receives wal at the version the collection tested — the alternative to today's direct host edge.
2.4 The friend closure — transitivity through friends-only edges
21For a node N, define grants(N) = the targets of N's edges carrying friend = true (explicit under the false default, or implied by a friends-only mark per F10 — an explicit friend = false beside the mark wins), plus N's friends list, minus N's unfriend list (§2.6). These are the packages N directly befriends.
22The friend closure C(R) of a root R is the least fixpoint of:
23seed: every G ∈ grants(R) is in C(R)
grow: if F ∈ C(R), and G ∈ grants(F), and F's edge F → G has access = "friends-only",
then G ∈ C(R)
24Reading: my friends are those I named; and when a friend marks one of its own befriended dependencies friends-only — «this one is part of my substance, shared with my friends» — that dependency becomes my friend too, recursively. The friends-only mark on F → G is thus simultaneously (a) the seepage gate of §2.2 and (b) the re-export of friendship that makes the closure transitive. This identification — the ordering message's per-edge «internal» mark IS access = "friends-only" — is owner-ratified (F1, 2026-08-23: «правильный термин — friends-only»).
25Public gives presence, not friendship (owner-ratified, F6). A package that seeps to R through public edges does not thereby join C(R), and its own friends-only edges stay closed to R unless R (or a chain of friends-only re-exports reaching R) befriends it explicitly. Friendship never grows through public or private edges — only through friends-only marks, which are explicit and rare (never a default), compounded by the friend = false default on the grant side. This is the built-in answer to «чтобы это замыкание не росло бесконтрольно»: the closure grows exactly along deliberate marks, backed by deliberate grants, and nowhere else.
26C(R) is a monotone least fixpoint over per-edge static predicates — deterministic and order-independent. With §2.9's path-stack masks in the picture, the closure and the reachable mask-states form one joint monotone fixpoint (alternating expansion until stable; masks are static declarations and both sets only grow, so termination and determinism hold; implementations dedup normalized mask-states) — the earlier draft's «computed strictly before the effective set» reading was corrected at the W1 landing, where the implementation surfaced the contradiction. Materialisation (§3) stays outside either computation. Cycles in requires remain a hard generate-time error exactly as in the linker today (PROP-034 §2.3).
2.5 Traversability and the effective set
27The effective set E(R) — the packages that exist from R's perspective — is everything reachable from R over traversable edges, along chains not killed by an exclude (§2.7). An edge P → Q is traversable for R iff any of the three rules below holds; private edges fail all three for P ≠ R, so a private dependency of a non-root package does not exist for the root — not resolved, not fetched, not materialised, not in any lane (§4.1–4.2).
28(1) P = R — the root's own edges always count
(2) access(P → Q) = "public" — unconditional seepage
(3) access(P → Q) = "friends-only" ∧ P ∈ C(R) — seepage to a friend of P's
29Everything downstream — version resolution, vibedeps/ materialisation, link-mode assignment, lane compilation, installed: predicates, the facts registry — operates on E(R) and nothing else. Visibility is computed once, first, and every other system consumes its output (§3).
2.6 unfriend — node-scoped pruning of the closure
30unfriend = ["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.
2.7 exclude — hard subtree exclusion
31exclude = ["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).
2.8 allow-friends — the provider's sealed circle
32Owner-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 — «годится»):
33[visibility]
allow-friends = ["org.a/x", "org.b/*"] # the permits list; "*" after a group = any member
- 34Three 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. - The check sits on the grant giver: a grant
N → 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. - The sealed payoff is enumerability:
G's inner world becomes computable fromG's manifest plus its permitted members' —vibe friends org.x/glists who may befriendG, who actually does, and through which chains. The exhaustive closure the owner names. - Trusted friends are non-sealed points by design:
Gcurates its immediate circle; a permitted friendFthat friends-only-marks its ownF → Gedge extendsG's substance toF's friends — vouching onward is exactly what admission to the circle licenses (as apermits-listed subtype may itself benon-sealed). A provider wanting no transit keeps its circle to leaves it trusts not to re-export. - The seal gates friendship, never delivery (pinned at the W6 landing). A sealed
Gstill 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). overridebreaks the seal — owner-ruled: «механизм override должен всё равно иметь возможность взломать эти рёбра». A node-targeted override entry —"org.x/g" = { allow-friends = "*" }(or a replacement list) — rewrites the permits list in the root's perspective, quietly, like every override (F9, §2.9).
2.9 override — the sanctioned break-in, anywhere in the graph
35Owner-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).
36Path-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).
37Syntax 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.
38A mid-graph override is contract; the root's is private. An [override] in a package's manifest changes what that package delivers — it is part of the package's versioned surface, subject to ##ACCESS-IS-SEMVER-SURFACE like any access change (expansive entries additionally ride at the breaker's own risk: the broken-into provider promised nothing). The consumer root's [override] binds nobody downstream and is versioned by nothing but its own repository. Observability is pull-based only, per the quiet ruling: lock provenance marks and vibe why … — via override of <node> answer whoever asks; nothing shouts at install time.
2.10 Worked example — the owner's chain
39The ordering message's chain A → B → C → D (D a practice), with each intermediate marking its dependency edge friends-only (each mark implying its own grant per F10): grants(A) ⊇ {B} (A's explicit grant on its own edge), so B ∈ C(A); B's edge B → C is friends-only and C ∈ grants(B), so C ∈ C(A); C → D likewise, so D ∈ C(A) — «загрузившись в D мы получим, что по цепочке транзитивности, D является другом для A». Traversal: A → B by rule (1); B → C by rule (3) with B ∈ C(A); C → D by rule (3) with C ∈ C(A) — so D ∈ E(A) and D materialises for A. If B had declared unfriend = ["…/C"], the chain would break at B for every root above B — while a sibling path A → B′ → C (B′ friends-only-marking C) would deliver C and D intact.
3. Layering — visibility above materialisation
40Owner ruling, adopted as the layer boundary: «признак типа static-transitive должен применяться уже ПОСЛЕ вычисления замыкания друзей, потому что замыкание друзей — это свойство логики связей в приложении, а транзитивная статичность — это свойство материализации, более логически низкая структура». The pipeline becomes:
411. visibility — compute C(R), then E(R) (this PROP; pure relations)
2. resolution — version-resolve E(R) only (PROP-003/017, unchanged in kind)
3. link modes — declared / suggested / default, per edge (PROP-009 §2.4, unchanged)
4. linker — zones, static-transitive forcing, dedup, (PROP-034/035 §12/038,
topo order, hoisting re-scoped to E(R))
5. emission — STATIC / INDEX lanes (unchanged)
- 42The
linkaxis (static/dynamic/static-transitive/static-hard) survives intact as materialisation strength within the visible graph: «возможность спускать вниз по иерархии признак статичности всё так же очень нужна… как отдельная ось, ортогональная нашему замыканию друзей». - Forcing never widens visibility. A
static-transitiveedge propagates staticness across traversable edges only: it can make a visible package static, never make an invisible package visible. The hybrid linker's forced-descent (resolve_zone, PROP-038 §2.2) simply walksE(R)'s subgraph instead of the raw requires graph; hoisting counts (PROP-038 §5.2) count withinE(R). - This ordering is THE-LAYER-LAW (PROP-048) applied to the pipeline itself: the logical stratum (who exists) is more stable and more upstream than the materialisation stratum (how it loads); a change in link mode never invalidates the visibility computation above it.
4. System interactions
4.1 Resolver and lock
43Version 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.
44The interleaving, named honestly. Computing E(R) needs edges; edges live in manifests of resolved versions; constraints come only from E(R) — so the walk is joint: the resolver expands the graph following only traversable edges, with C(R) recomputed monotonically as newly-resolved manifests contribute grants and marks (all predicates are static edge attributes of already-chosen nodes, so the joint fixpoint stays deterministic). Reading a manifest to learn that its edge is private is a metadata read, not materialisation — package content is fetched for E(R) members only. The Cargo lesson (RFC 1977 lost six years to resolver entanglement; RFC 3516 lives by decoupling) is honoured structurally: visibility stays a pure edge predicate the walk consults — never a quantity the version solver optimises over.
4.2 Materialisation and vibedeps/
45vibedeps/ 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.
4.3 PROP-049 under visibility — what the genre machinery still does
46The ordering message asks whether «наша система с контролем flows» is still needed. No standalone "flows control" system exists in the spec tree (verified 2026-08-23: PROP-009's flow is an ordering category; PROP-028's flows are package kinds); the referent is read as the PROP-049 genre machinery — installed: predicates, snippet fragments, concepts, and the presupposition gate — built precisely against the WAL seepage this PROP now fixes structurally. Fork F7 confirms the referent.
47Recommendation: keep PROP-049, re-scoped — the two systems solve different halves. Visibility controls structural arrival (whether a package exists in E(R)); the genre machinery controls textual behaviour (what a snippet may presuppose, and how text adapts to a neighbour's presence). With visibility live: (a) installed: predicates and fragments remain THE adaptivity mechanism — an installed: predicate now queries E(R), and a fragment binding to an absent friend stays physically omitted exactly as today; (b) the presupposition gate remains the authoring lint that catches a snippet textually assuming what its package did not lawfully receive; (c) what dissolves is the unbundling pressure — a future redbook-like collection may keep a wal-like member on a friends-only/private edge instead of expelling it.
48Direction (F7 follow-up): concept-gated fragments. Today the concepts array feeds exactly one consumer — the presupposition gate's dictionary (crates/vibe-check/src/checks/snippet_presupposition.rs): lexeme → owning package, matched against unconditional snippet text. A natural completion is when = "concept:WAL" — a fragment gated on «some package in E(R) declares this concept», resolving a discipline as an interface rather than a named implementation (installed: stays the exact-package form). Several packages could then carry one discipline interchangeably. Deferred to the owner's F7 ruling.
49One PROP-049 rule must tighten. The presupposition lint currently exempts mentions of the package's own declared dependencies. Under visibility that exemption is too wide: a mention of an own private dependency in unconditional snippet text presupposes a package that never reaches any consumer — lawful only inside an installed:-gated fragment (which, for a private dependency, fires only in the declarant's own dev world, §4.4 — exactly right). The exemption narrows to edges that seep (public, or friends-only). This is a concrete PROP-049 amendment shipped with the implementation wave, in the warning genre of ##CONCEPTS-GATE-SOFTENED.
50Owner-ruled (F7, 2026-08-23): the gate speaks in warnings, with lawful homonymy, owner-dedup, and a subtree mute. The owner's fear is exact — «термин WAL вообще супер ходовой, его многие используют в разных смыслах» — so: (a) a foreign-concept mention is a warning («пакет X использует концепт из пакета Y»), never an error; (b) homonymy is lawful — a package that declares the lexeme in its own concepts owns the word in its own world and triggers nothing; (c) dedup across owners — when several packages declare one lexeme, a legitimate relation to any one of them (own declaration, or a seeping edge to one owner) silences the warning — «достаточно лишь одного» — and an unexplained use warns once, naming all owners, not once per owner; (d) node-level ignore-concept-warnings = ["WAL", …] mutes the named concepts for the entire subtree hanging off that manifest. The machinery stays under this softening — «фича полезная, но очень нишевая».
4.4 Equipotence and the dev world
51Rule (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.
4.5 vibefacts
52The adoption-facts registry (PROP-046) keys per-source files by installed packages; its universe follows E(R) mechanically. Registry entries for packages that leave the effective set surface through the existing facts_sync / lifecycle machinery (PROP-046 L5) — no new mechanism, one new reason entries become stale.
4.6 Collections and families
53A PROP-028 collection's whole point is aggregation: its member edges are the type case for access = "public" (redbook's ~21 exact-pinned members must seep to redbook's consumers — that is what depending on the collection means). A family's internal shared core is the type case for friends-only (members befriend each other through the family's marks; outsiders opt in or see nothing). Migration inventory in §6.
5. Prior art — what the neighbours teach
54Three research sweeps (2026-08-23; JVM lineage — JPMS / sealed / Kotlin / Swift; module-graph lineage — OSGi / Eclipse PDE / Bazel / Buck / Pants; dependency-manager lineage — Gradle / Maven / Cargo / npm / C++ friend and C++20 modules) ground this section; the full worker reports are archived in vibevm/vibespecs/research/dependency-visibility-2026-08/. The map, on the two axes that matter — who declares, and what a denial costs:
| System | Direction | Closest primitive to ours | Verdict used here |
|---|---|---|---|
JPMS requires transitive |
provider re-export | the closure grow rule — same recursive shape |
adopt shape; adopt its usage norm |
JPMS exports … to |
provider allowlist | friends-only (opposite direction) |
unknown target = warning, not error |
Java sealed … permits |
provider allowlist | friends list | allowlist needs one maintenance domain — ours has it (the consumer's own file) |
| Swift SE-0409 access-level-on-imports | consumer, per-edge | access — the closest precedent alive |
validates the core; they deferred the default flip twice |
Swift @_spi |
bilateral handshake | friends-only ∧ friendship |
conjunction, adopted (§2.5) |
OSGi Require-Bundle; visibility:=reexport |
provider re-export | friends-only as re-export |
its irreversibility lesson → ##ACCESS-IS-SEMVER-SURFACE |
Eclipse x-friends |
provider allowlist | friends-only | provider lists rot because the payer can't edit them — the inversion argument |
Bazel deps + exports* chains |
provider re-export | the closure formula, verbatim | battle-tested shape of §2.4 |
Buck within_view |
consumer cap, wins conflicts | exclude |
exclusion beats any grant, adopted |
| Pants dependency/dependents rules | both ends must agree | rule (3) of §2.5 | conjunction as the core evaluation rule |
Gradle api/implementation |
provider | public/private |
the compile-removal precedent: leakage-by-default is unpayable |
Maven <exclusions> / enforcer |
consumer edge / assertion pass | exclude / deferred deny-list |
two-layer split, adopted in F4 |
| Cargo RFC 1977 → 3516 | provider mark | public |
six years lost to resolver entanglement — keep visibility a pre-filter |
npm exports map |
provider surface seal | (none yet) | sub-package surfaces — deferred direction |
C++ friend |
provider, deliberately non-transitive | the anti-model | what non-transitivity protects → ##CLOSURE-DRIFT-CONTROL |
56Adopted into the model on prior-art strength: (1) the conjunction rule — an edge materialises iff provider access permits ∧ consumer grant permits (Pants' "both ends must agree", Swift @_spi's handshake) — §2.5 rule (3) is exactly this; (2) the closure shape — first hop a direct grant, then zero-or-more re-export marks — is Bazel's deps-then-exports* and JPMS implied readability, both battle-tested recursive; (3) friend groups need no new primitive — befriending a collection whose member edges are friends-only already delivers the members through the ordinary closure, giving Bazel-package_group-style composition for free and dodging Swift's rejected "large, complex manifests mingling all the layers"; (4) unknown allowlist targets warn, never fail (JPMS qualified-export precedent) — a friends/unfriend entry naming a package absent from every chain is a lint, §7; (5) denial is legibility, not failure — every surveyed system hard-fails because a missing symbol breaks a build; a missing prompt lane merely changes what loads, so our enforcement output is the pruning report (vibe why, §7), and only graph cycles remain hard errors.
57An access mark is versioned surface. OSGi's verdict on reexport — «once you have added re-export you cannot remove it without considering the corresponding API change» — and Cargo's public = true semver reasoning transfer whole: widening an edge (private → friends-only → public) is a feature; narrowing one is a breaking change of the declaring package, versioned like any contract change. The publish gate (C6 family) checks access narrowing against the previous published version.
58What C++'s non-transitivity protected, restored by other means. C++ keeps friend non-transitive so the set that can touch you stays enumerable by reading one file; our marks keep the closure enumerable but not local — a mid-graph package can widen C(R) in a patch release the root never reads. Three controls restore the audit: (a) the lock records E(R) with per-package lane cost, so vibe update surfaces closure drift as a reviewable diff («+2 packages, +3.1k static-lane tokens»); (b) ##ACCESS-IS-SEMVER-SURFACE makes silent widening a versioning violation; (c) an optional per-lane token budget cap (a PROP-048 direction) turns runaway growth into a loud stop.
59Authoring 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.
60Deliberately not taken: OSGi uses:-style implicit constraint propagation (prompt text has no class space — a budget concern must never become a global satisfiability problem); Kotlin's compilation-unit boundary (visibility binds to declared edges and coordinates, never to «whatever landed in one lane»); graph-global unification of any visibility knob (Cargo's feature-unification scar — closures are per-root by construction, §2.1, and never merge); provider-side allowlists (F8); npm-exports-style named sub-package surfaces — a real direction (a grant admitting one declared fragment rather than a package's whole contribution) that composes naturally with PROP-049 fragments, deferred until the package-level system has run in anger.
6. Migration
61Owner ruling (2026-08-23) — no legacy read, no backward compatibility: «нужно переделать существующие пакеты, никакого legacy-чтения и обратной совместимости; система, которую мы сейчас сделаем, становится основной и не откатываемой никуда; пакеты предыдущих форматов не имеют смысла (поэтому их нужно улучшить)». The strict reading — with the ruled defaults, access = "public" / friend = false (##DEFAULT-PUBLIC-RATIONALE, F3) — is the only reading; the implementation wave marks the tree's manifests in the same wave the machinery lands. This overrules the draft's legacy-read/ratchet proposal.
62Why the §5 flag-day graveyard does not forbid this: those lessons price a default flip across an open ecosystem of foreign artefacts (JPMS: the world's jars; Swift: every existing package). This tree is a closed world — every manifest that exists lives in this repository, C6 publication is frozen, and there are no external consumers yet — so the «flag day» is an ordinary all-packages refactoring wave over owned files. The public presence default (##DEFAULT-PUBLIC-RATIONALE) softens it further: an unmarked manifest behaves exactly as today, so a partially-marked tree merely keeps seeping where not yet narrowed — degraded budget, never a broken world. The atomic wave + §7 golden snapshot remain the standard for landing cleanly, as review hygiene rather than a survival condition.
63Re-marking inventory (in-wave per ##MIGRATION-NO-LEGACY; under the public default the work is deliberate narrowing, not blanket re-annotation): redbook's 21 member edges, stack aggregators' cores, delegation-first → delegation-rules, wal-specspaces → wal, host root edges — all correct under the default, no marks needed; dev-/tooling edges across the tree → explicit private; git-practices family internals → first candidate for friends-only dogfooding; and the door PROP-049's unbundling closed re-opens as an option — a redbook-like collection may again carry a wal-like member on an explicit private/friends-only edge, a separate decision for its author. The 8 world packages' [boot_snippet] link = "static" self-suggestions are materialisation-axis and untouched.
64Implementation shape (post-approval, separate wave, ask-first per Rule 4 only where its list applies): (1) vibe-core manifest schema — the new edge/node fields, strict semantics only; (2) C(R)/E(R) computation as a new pure module consumed by resolver + bootgen; (3) linker re-scoped to E(R); (4) PROP-049 exemption narrowing; (5) package-tree/vibe why observability (§7); (6) re-marking of every package manifest in the tree (the inventory above); (7) the golden landing snapshot + measurement pass (§7). Steps 1–5 are machinery, step 6 is the flip, and the wave lands atomically — no commit leaves the tree half-marked (##migration-flag-day-scope). The wave-by-wave build plan elaborating this shape is vibevm/vibespecs/terraforms/VISIBILITY-BUILD-PLAN-v0.1.xml.
7. Verification and measurement — «РЕАЛЬНО ХОРОШО»
65The owner's bar is explicit: «перестроить статические и динамические лоадеры и проверить, что всё работает РЕАЛЬНО ХОРОШО. Измерить, проверить, посмотреть на тестовых примерах». The implementation wave carries, as first-class deliverables:
- 66Golden chains. Fixture worlds for every §2 rule — the owner's
A → B → C → Dchain verbatim (test №1), the unfriend break, the diamond-with-exclude, public-presence-without-friendship, the dev-world flip (same package as root vs as dependency) — each asserting the exactE(R), lock content, and lane bytes. - Determinism and the landing snapshot. Re-runs over identical inputs are byte-identical (the PROP-038 §5 stability bar, extended to visibility inputs). Since there is no legacy mode (##MIGRATION-NO-LEGACY), the correctness artefact is the golden snapshot of the re-marked world: the wave captures the intended
E(R)and lane composition per root, the one-time diff against the pre-visibility lanes is read by hand at landing (expected: WAL-class removals, zero accidental losses), and a panel cell pins the snapshot thereafter. - Measurement. Before/after per-root metrics, recorded in the wave's harvest: effective-set package count, lane byte sizes, fetch/materialise counts and install wall-time. The tokenomics claim (§1) must show up as numbers, not prose.
- 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. - Closure-drift visibility. The lock carries
E(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. - Hygiene lints (built as the
visibility_hygienecell; the per-grant lane-cost figure is deferred — BACKLOG B-103). Deadfriends/unfriend/excludeentries naming packages that never appear in any chain (warning, per the JPMS qualified-export precedent); friends-only edges whose declarant nobody befriends (unreachable sharing); afriend = falseedge to a package with no friends-only edges (a no-op mark); grants whose admitted chains contribute no lane text this root ever loads — each reported with the lane cost the grant admits, so an unused or cost-heavy friendship is visible (the consumer-side mirror of Eclipse's rotting x-friends lists). Advisory, invibe check.
8. Rollback
67By owner ruling — none. Until implementation this document changes nothing. Once the wave lands, visibility is the primary and irreversible model — «становится основной и не откатываемой никуда» — and pre-visibility manifest reading retires with it; there is no legacy mode to fall back to, and the old formats are not preserved («пакеты предыдущих форматов не имеют смысла»). formats/EPOCHS.toml is untouched; the safety that replaces rollback is the atomic wave + golden snapshot of §6/§7.
9. Open forks for the owner
68The reconstruction forks, with the owner's 2026-08-23 rulings recorded; F7–F9 remain live:
- 69F1 — «internal» ≡
access = "friends-only". RATIFIED («правильный термин — friends-only»): one mark is both the seepage gate and the friendship re-export (§2.4); no separateinternalfield exists. - F2 — the anti-list is
unfriend. RATIFIED;enemyis retired. - F3 —
frienddefault. RE-RULED 2026-08-23 tofalse. The originaltruebelonged to the one-field paradigm; with access and friendship separated, friendship is a raised privilege granted only explicitly («Не давать повышенных привилегий там, где они могут и не пригодиться»). Paired with the same ruling's access default flip topublic(##DEFAULT-PUBLIC-RATIONALE): presence generous, friendship stingy. See F10 for the one ergonomic consequence. - F4 —
excludeis per-edge, Maven-style, arbitrarily deep within the edge's subtree. RATIFIED («можно per-ребро делать глубокие эксклюды по иерархии — это не глобальный deny-list, это сужение в рамках поддерева»). No global deny-list. F9 may re-house the syntax insideoverride; the semantics stand. - F5 — migration. OVERRULED by the owner: no legacy read, no backward compatibility, the system is primary and irreversible; every existing package is re-marked in the landing wave (##MIGRATION-NO-LEGACY, ##migration-flag-day-scope). The draft's autodetect+ratchet proposal is retired.
- F6 — public gives presence, not friendship. RATIFIED («access и friend разные понятия, public даёт присутствие но не дружбу»); the composition of the two marks on one edge is pinned as ##PUBLIC-PLUS-FRIEND.
- F7 — PROP-049's fate. RULED 2026-08-23: keep, softened. The gate becomes a warning system with lawful homonymy, owner-dedup and the
ignore-concept-warningssubtree mute — ##CONCEPTS-GATE-SOFTENED carries the four rulings; ##DEPS-EXEMPTION-NARROWS joins the same genre. ##CONCEPT-GATE-DIRECTION (when = "concept:X") stays a sketch, unruled. - F8 — provider-side allowlists. RULED and design RATIFIED 2026-08-23 («спроектируй механизм allow-friends…» → design delivered as §2.8 → «годится»): node-level permits list; absent = open,
[]= sealed, list = the circle; checked on the grant giver; rejected grant = warning; enumerable viavibe friends; trusted friends are non-sealed transit points;overridebreaks the seal by node-targeted entry. - F9 —
override: the official, QUIET break-in, lawful ANYWHERE. RULED 2026-08-23 (two passes). First pass: deliberate developer break-in is not a threat model — «он всё равно может пойти и руками поправить любые свойства пакетов на диске», as Java's reflection era proved; languages with no sanctioned privacy story (Python) breed confusion about the intended surface; so vibevm ships an official break-in verb instead of hacks, and ships it quiet — no install-time notices («постоянный мусор… иллюзия безопасности»), pull-based provenance only.excludestays a separate directive («список exclude убирать не надо»). Second pass: the draft's root-only recommendation is overruled — «разрешён не только в корневом манифесте, а где угодно»; the any-node semantics (path-stack masks, nearer-to-root wins, existential-over-chains, mid-graph override as versioned contract) is the body's §2.9. The subtraction-vs-expansion analysis stands: expansive entries breach ##ACCESS-IS-SEMVER-SURFACE at the breaker's own risk. - F10 —
friends-onlyimpliesfriend = trueon its own edge. RATIFIED 2026-08-23 (##FRIENDS-ONLY-IMPLIES-FRIEND): one mark per hop makes the chain; explicitfriend = falsebeside the mark wins, producing the lawful no-vouch cell of ##ACCESS-FRIEND-MATRIX.
10. Version history
- 702026-08-23 (fourth pass) — design closed, build planned. F10 ratified (the implication); §2.8
allow-friendsratified as designed; F9 re-ruled to any-node override — root-only overruled, the path-stack semantics (nearer-to-root wins, existential over chains, mid-graph override is versioned contract) lands as §2.9; the worked example moves to §2.10. The access×friend matrix (##ACCESS-FRIEND-MATRIX) and the transit/use-case facts (##TRANSIT-WITHOUT-DECLARATION, ##FRIENDS-ONLY-USE-CASES) had landed between passes. Build plan authored:vibevm/vibespecs/terraforms/VISIBILITY-BUILD-PLAN-v0.1.xml; implementation awaits the owner's start word. - 2026-08-23 (third pass) — defaults recast, remaining forks ruled. Owner re-rulings:
accessdefaults public (##DEFAULT-PUBLIC-RATIONALE — loaders must not silently lose what was always available; the observability asymmetry seals it),frienddefaults false (F3 re-ruled — friendship is a raised privilege; presence generous, friendship stingy). F7 ruled: the concepts gate softens to warnings with lawful homonymy, owner-dedup andignore-concept-warnings(##CONCEPTS-GATE-SOFTENED). F8 ruled:allow-friendsdesigned (§2.8) — sealed-permitsshape, override-breakable. F9 ruled in substance: the override is official and quiet (no install notices; pull-based provenance only);excludestays separate; root-only stands as recommendation. F10 filed (friends-only implies its own grant). Migration softened accordingly: an unmarked tree seeps as today instead of breaking (##migration-flag-day-scope); the wave's work is deliberate narrowing. - 2026-08-23 (later the same day) — core ratified by the owner. F1 (friends-only ≡ the re-export mark), F2 (
unfriend), F3 (friend = truedefault on own edges), F4 (per-edge deep-subtreeexclude) and F6 (access ⊥ friendship; ##PUBLIC-PLUS-FRIEND added) ruled as drafted. F5 overruled: no legacy read, no backward compatibility, irreversible — ##MIGRATION-NO-LEGACY replaces the autodetect+ratchet proposal; §7/§8 re-anchored to the atomic wave + golden snapshot. F7 (concepts mechanics + ##CONCEPT-GATE-DIRECTION) and F8 (allow-list mechanics) explained, rulings pending; F9 (override, the root-side sanctioned break-in) filed with the subtraction-vs-expansion analysis. - 2026-08-23 — drafted (owner-ordered). The visibility model: per-edge
access(public / private-default / friends-only), consumer-declared friendship (per-edge flag + node list), the friend closure as a least fixpoint over friends-only re-exports,unfriend(node-scoped) andexclude(edge-scoped) growth controls, root-driven materialisation. Layering: visibility above materialisation (owner ruling); thelinkaxis survives re-scoped to the effective set. Interactions: resolver/lock/vibedeps onE(R)only; PROP-049 kept re-scoped with the deps-exemption narrowed; the dev-world reading resolves the equipotence wave's open tail. Migration by per-manifest legacy read; verification plan with golden chains, byte-stability, measurement, andvibe why. Prior-art synthesis and §9 forks await the owner.