<?xml version="1.0" encoding="UTF-8"?>
<spec xmlns="https://vibevm.org/spec/1">
  <title id="root">PROP-038: Hybrid boot linking — per-package compilation units with soft/hard static edges</title>
  <status stage="impl" state="done" comment="B0 2026-07-24: IMPLEMENTED 2026-07-15, all five campaign phases shipped"/>
  <p p="1"><fact id="status-line" status="impl/done">**Status:** IMPLEMENTED — 2026-07-15 (all five campaign phases shipped, `d487d4e`…`381095e`; see §6). Requirements captured from an owner design dialogue; **ratified by the owner's directive to implement in full** (2026-07-15, "реализуй всю гибридную линковку, включая спеки, код и тесты"). The §5 open questions are **resolved** (Phase 0 of the campaign, recorded inline in §5 with each resolution); implementation follows the [HYBRID-LINKING campaign](../../../legacy-spec/terraforms/HYBRID-LINKING-PLAN-v0.1.md).</fact></p>
  <p p="2"><fact id="extends" status="spec/done">**Extends:** [PROP-009](PROP-009-loading-model.xml) (the loading model — the `STATIC.md` / `INDEX.md` artifacts §2.3, the `static` / `dynamic` link types §2.4), [PROP-035](PROP-035-spec-compiler.xml) (the two-mode boot linker §2, `#use` §7.2, the `@spec` read-set §7.4, link tables §10).</fact></p>
  <p p="3"><fact id="INPUT-IS-THE-EFFECTIVE-SET" status="impl/done">**Input narrowed by the visibility layer (2026-08-23, PROP-050 §3):** the unit table this linker compiles is built from the resolved `ResolvedDep` slice, which since the W2 landing carries only the consumer root's **effective set E(R)**. Zones, `static-transitive` forcing, hoisting counts and surfaced dynamic edges therefore all operate within `E(R)` by construction — forcing can make a visible unit static, never make an invisible one visible (PROP-050 ##FORCING-NEVER-WIDENS).</fact></p>
  <p p="4"><fact id="supersedes-line" status="spec/done">**Supersedes / evolves:** [PROP-034](PROP-034-transitive-links-boot-graph.xml) — its **single global** static-link graph and the precedence lattice ([§2.2](PROP-034-transitive-links-boot-graph.xml#precedence)) are replaced by **per-edge recursive** linking plus hoisting (§2.2, §2.4 below). PROP-034's dedup + topological-order + cycle-rejection invariants are retained, applied **per compilation unit**.</fact></p>
  <p p="5"><fact id="related" status="spec/done">**Related:** [PROP-017 §3](../vibe-resolver/PROP-017-resolvo-resolver.xml#encoding) (resolvo — the single-version-per-name invariant this rests on), [PROP-011 §2.4](PROP-011-incremental-install.xml#boot-regen) (whole-tree boot regeneration — revised here to a dirty-subgraph), [PROP-022](PROP-022-materialization-modes.xml) / [PROP-014](../../../vibepacks/org.vibevm.ai-native/core-ai-native/v1.0.0/vibevm/vibespecs/mechanisms/PROP-014-specmap-bidirectional-traceability.xml) (`content_hash`, the specmap/link-table index), [`vibevm/vibespecs/design/loading-and-boot-model.xml`](../../design/loading-and-boot-model.xml) (the static/dynamic-linking metaphor this PROP completes).</fact></p>
  <section id="motivation" title="1. Motivation — the boot must link like a real linker, per unit">
    <list ordered="false" p="6">
      <item><fact id="status-quo-linking" status="impl/done">PROP-009 gives each dependency edge an inclusion type and emits **one** `STATIC.md` + `INDEX.md` per entry-point workspace node. PROP-034 resolves the whole closure as one **global** static-link graph, seeded from the root manifest.</fact></item>
      <item><fact id="two-limitations" status="impl/done">Verified against the shipped `bootgen` (2026-07-15), two limitations block the model the owner wants:</fact></item>
    </list>
    <list ordered="true" p="7">
      <item><fact id="LIMIT-ROOT-ONLY" status="impl/done">**Static propagates only from the root.** The `static-transitive` closure is seeded **exclusively** from the root manifest's direct edges; `link` declarations **inside** an intermediate (dynamically-linked) package are never read for boot. So a `dynamic`-linked package `A` cannot declare "I statically link my own dependency `B`" — `B` falls back to `dynamic`. The effective-mode lattice (PROP-034 §2.2) is only half-implemented: "static wins", but only from the root.</fact></item>
    </list>
    <list ordered="true" p="8">
      <item><fact id="LIMIT-GLOBAL-STATIC" status="impl/done">**A single global `STATIC.md` cannot express *local* static.** Modes are a global property of a node. "`B` is static" means "`B` is in the one root `STATIC.md`, read first, always" — even when `B`'s parent `A` is `dynamic` and may never load. There is no notion of "static **within** `A`": static compiled *relative to* a package, loaded *with* that package, and only when it loads.</fact></item>
    </list>
    <list ordered="false" p="9">
      <item><fact id="OWNER-TARGET" status="impl/done">The owner's target is **local nested static linking** — a dynamically-linked package that statically links its own dependencies, recursively, exactly as a real linker composes objects into a `.so` (statically-linked, inside) while leaving other `.so`s as `DT_NEEDED` dynamic references (late-bound by the loader).</fact></item>
      <item><fact id="HYBRID-LINKER" status="impl/done">This PROP makes boot a **hybrid linker**: it composes AOT (static, *within* a compilation unit) and JIT (dynamic, *across* unit boundaries) **at every edge**, and adds a soft/hard dedup axis on the static side.</fact></item>
    </list>
  </section>
  <section id="decisions" title="2. Decisions">
    <section id="units" title="2.1 Every materialised package is a compilation unit">
      <p p="10"><fact id="UNIT-PER-PACKAGE" status="impl/done" action="continue" actionstage="doc" audience="user">**Decision.** Every package materialised under `vibedeps/` carries its **own** boot artifacts — `vibedeps/&lt;slot&gt;/spec/boot/STATIC.xml` (what is compiled **into** this unit, verbatim) and `.../INDEX.md` (this unit's **external dynamic** references, resolved when the unit loads) — not only entry-point workspace nodes.</fact></p>
      <p p="11"><fact id="UNITS-CHANGES-009" status="impl/done">This changes PROP-009 §2.3's "for every entry-point node" to "for every compilation unit (entry-point node **or** materialised package)".</fact></p>
      <p p="12"><fact id="UNIT-SELF-CONTAINED" status="impl/done">A unit's `STATIC.md` is self-contained and reversible (open/close markers, PROP-035 §11): reading it, an agent gets this package and everything statically linked into it, in dependency order, once each — the PROP-034 dedup + topological-order + cycle-rejection invariants, applied **within the unit**.</fact></p>
      <p p="13"><fact id="UNIT-ARTIFACT-STATIC-CONSUMER-ELIDES" status="spec/work">The unit artifact serves **dynamic** consumers (an `INDEX.md` reference loads the whole zone through it) and standalone unit reads; a **static** consumer whose lane already compiles the unit's zone member-by-member **elides** the aggregate entry to a provenance stub instead of embedding the artifact — the once-each rule of PROP-009 §2.3 `##STATIC-EMITS-ONCE-EACH` (B-006, owner-approved 2026-08-04), applied at compose time and distinct from §2.4's hoisting, which shares one copy *across* consumers rather than deduplicating within one lane.</fact></p>
    </section>
    <section id="edge-recursion" title="2.2 The edge is the linker instruction; compilation is recursive and dynamic-bounded">
      <p p="14"><fact id="EDGE-IS-INSTRUCTION" status="impl/done" action="continue" actionstage="doc" audience="user,author">**Decision.** `link` is a property of the **edge** (consumer-side, declared in the parent's manifest), never baked into the pulled package (as PROP-034 §2.1 already states). A unit `P` is compiled by walking its **own** direct edges `P→X`:</fact></p>
      <list ordered="false" p="15">
        <item><fact id="EDGE-STATIC" status="impl/done">**`static`** — `X`'s `STATIC.md` is compiled into `P`'s `STATIC.md`. Compilation **recurses down** `X`'s own static edges; a nested `dynamic` edge inside `X` breaks the recursion (that target stays an `INDEX.md` reference). `static` therefore **respects** the modes below it.</fact></item>
        <item><fact id="EDGE-DYNAMIC" status="impl/done">**`dynamic`** — `X` is **not** compiled; it becomes an `[[entry]]` in `P`'s `INDEX.md`. The static zone **breaks** at this edge.</fact></item>
        <item><fact id="EDGE-STATIC-TRANSITIVE" status="impl/done" action="continue" actionstage="doc" audience="author">**`static-transitive`** — `X` and its **entire** subtree are forced `static`, **ignoring** any `dynamic` edges inside — "rewrite the whole tree under `X`". This is the one mode that overrides nested breaks.</fact></item>
      </list>
      <p p="16"><fact id="STATIC-VS-TRANSITIVE" status="impl/done">The difference between `static` and `static-transitive` is exactly this treatment of nested `dynamic` edges: `static` honours them (breaks), `static-transitive` overrides them (forces).</fact></p>
      <p p="17"><fact id="worked-example-lead" status="impl/done">**Worked example** — `root → A(dynamic) → B(static) → C(dynamic) → D(static-transitive)`:</fact></p>
      <table p="18">
        <tr>
          <td>Unit artifact</td>
          <td>Contains</td>
          <td>Because</td>
        </tr>
        <tr>
          <td><fact id="ROW-EX-ROOT" status="impl/done">`root/…/STATIC.md`</fact></td>
          <td><fact id="ROW-EX-ROOT-CONTAINS" status="impl/done">— (no A, B, C, D)</fact></td>
          <td><fact id="ROW-EX-ROOT-BECAUSE" status="impl/done">`root→A` **dynamic** → break; A is a reference in `root/INDEX.md`</fact></td>
        </tr>
        <tr>
          <td><fact id="ROW-EX-A" status="impl/done">`vibedeps/A/…/STATIC.md`</fact></td>
          <td><fact id="ROW-EX-A-CONTAINS" status="impl/done">**A + B** (no C)</fact></td>
          <td><fact id="ROW-EX-A-BECAUSE" status="impl/done">`A→B` **static** → B compiled in; `B→C` **dynamic** → C breaks</fact></td>
        </tr>
        <tr>
          <td><fact id="ROW-EX-B" status="impl/done">`vibedeps/B/…/STATIC.md`</fact></td>
          <td><fact id="ROW-EX-B-CONTAINS" status="impl/done">B</fact></td>
          <td><fact id="ROW-EX-B-BECAUSE" status="impl/done">`B→C` **dynamic** → C is a reference in `B/INDEX.md`</fact></td>
        </tr>
        <tr>
          <td><fact id="ROW-EX-C" status="impl/done">`vibedeps/C/…/STATIC.md`</fact></td>
          <td><fact id="ROW-EX-C-CONTAINS" status="impl/done">**C + D + all under D**</fact></td>
          <td><fact id="ROW-EX-C-BECAUSE" status="impl/done">`C→D` **static-transitive** → forces the subtree</fact></td>
        </tr>
      </table>
    </section>
    <section id="modes" title="2.3 Two static modes — `static-soft` (default) and `static-hard`">
      <p p="19"><fact id="TWO-STATIC-MODES" status="impl/done">**Decision.** The static side has two modes, differing in **where** duplication is deduplicated:</fact></p>
      <list ordered="false" p="20">
        <item><fact id="MODE-STATIC-SOFT" status="impl/done" action="continue" actionstage="doc" audience="author">**`static-soft`** — **the default**, the meaning of a bare `link = "static"`. Hoisting dedup at **compile time**: a package statically linked by more than one consumer is **hoisted** to a shared location (§2.4) and linked **once**; each consumer references it. Deterministic; does not depend on read-time behaviour.</fact></item>
        <item><fact id="MODE-STATIC-HARD" status="impl/done" action="continue" actionstage="doc" audience="author">**`static-hard`** — explicit opt-in (`link = "static-hard"`). **Pure local** compilation: every consumer compiles the package into its own `STATIC.md` independently, with no hoisting. Duplication is deduplicated at **read time** by the read-set (§2.9).</fact></item>
      </list>
      <p p="21"><fact id="SOFT-DEFAULT-WHY" status="impl/done">**Why soft is the default (owner decision, 2026-07-15).** A forgotten qualifier must fail toward **correctness**, not toward implicit duplication.</fact></p>
      <list ordered="false" p="22">
        <item><fact id="duplication-hazard" status="impl/done">When the same package is compiled into several units unhoisted, the model sees the same prompt several times and can be confused about which copy is authoritative — a correctness hazard the owner weighs above the "explicit-over-implicit" cost of a smart default.</fact></item>
        <item><fact id="HARD-REMAINS" status="impl/done">`static-hard` remains for the deliberate case where a package must load **only** with its consumer (lazy locality preferred over dedup) even at the price of on-disk duplication.</fact></item>
      </list>
    </section>
    <section id="hoisting" title="2.4 Soft hoisting targets the LCA of the static-zone, not always the global root">
      <p p="23"><fact id="HOIST-LCA" status="impl/done">**Decision.** A hoisted package rises to the **least common ancestor within a continuous static zone** of its consumers, not unconditionally to the global root:</fact></p>
      <list ordered="false" p="24">
        <item><fact id="HOIST-WITHIN-ZONE" status="impl/done">**Within one static zone** (consumers share a static ancestor `Z` reached by an unbroken chain of static edges) → hoist into `Z`'s `STATIC.md`. Dedup achieved **and** the package still loads only when `Z` loads: **laziness is preserved**. Within-zone hoisting is free and always done.</fact></item>
        <item><fact id="HOIST-CROSS-ZONE" status="impl/done">**Across dynamic zones** (the consumers' common static ancestor does not exist because a `dynamic` edge separates them) → the only shared always-loaded location is the **global root** `STATIC.md`, and hoisting there makes the package **eager** (loaded even when its dynamic consumers are not). This is the one real cost of soft, paid only for cross-zone sharing.</fact></item>
      </list>
      <p p="25"><fact id="hoist-consequences-lead" status="impl/done">**Consequences, all deliberate:**</fact></p>
      <list ordered="false" p="26">
        <item><fact id="HOIST-GLOBAL-PASS" status="impl/done">Hoisting needs a **global pass** counting the static-consumers of each package (partially re-introducing global analysis the per-edge model otherwise avoids) — the price of compile-time dedup. Recorded as a change-detection cost in §2.7.</fact></item>
        <item><fact id="HOIST-TRANSITIVITY" status="impl/done">**Hoist transitivity.** Hoisting `L` hoists `L`'s own static sub-zone with it (else the hoisted `L` references code not present at the hoist point).</fact></item>
        <item><fact id="HOIST-BARRIER" status="impl/done">A `static`-declared package can be **hoisted past** an edge that named it `static` only within its static zone; a `dynamic` edge is always a hoist barrier (crossing it is the eager cross-zone case above).</fact></item>
      </list>
    </section>
    <section id="markers" title="2.5 Hoist markers — the two ends of a lifted edge">
      <p p="27"><fact id="HOIST-MARKERS" status="impl/done">**Decision.** When soft hoists `L` out of a unit `P`'s local `STATIC.md`, two markers preserve correctness — the reversible two-ended shape PROP-035 §7/§11 already defines:</fact></p>
      <list ordered="false" p="28">
        <item><fact id="MARKER-USE" status="impl/done">**In `P`'s `STATIC.md`**, where `L`'s text used to be: a **`#use spec://…/L`** directive (PROP-035 §7.2). It preserves the `P→L` graph edge locally and tells the agent "`L` is part of me; its text is lifted and already read above — do not duplicate." The read-set (§2.9) gates the re-read, so no duplication reaches context.</fact></item>
        <item><fact id="MARKER-SHARED-BY" status="impl/done">**In the hoist target** (`Z`'s or the root's `STATIC.md`), at the lifted block: a **shared-by hint comment** naming the consumers (`shared by P, Q, R`). It explains to the model why `L` is here and not local, and asserts this is **one shared version** — not a duplicate to reconcile.</fact></item>
      </list>
      <p p="29"><fact id="MARKERS-REGENERATED" status="impl/done">Both markers are generated, are part of the reversible marker set, and must be regenerated on recompilation.</fact></p>
    </section>
    <section id="single-version" title="2.6 The single-version invariant this rests on">
      <p p="30"><fact id="SINGLE-VERSION-INVARIANT" status="impl/done">**Decision (recording a closed question).** Soft dedup is correct **because** the resolver guarantees **one version per `(kind, name)`** across the workspace — resolvo enforces single-version-per-name automatically (PROP-017 §3), and genuinely incompatible constraints fail as `Unsatisfiable` (PROP-017 §2.4) rather than coexisting. Therefore:</fact></p>
      <list ordered="false" p="31">
        <item><fact id="ONE-SHARED-VERSION" status="impl/done">A hoisted package is one shared version; there is never "two versions of `B` side by side" in a `STATIC.md`.</fact></item>
        <item><fact id="HINT-NOT-NEEDED" status="impl/done">The proposed "group different versions together + a divergence hint" feature is **not needed** — the situation it guards against cannot occur (confirmed 2026-07-15). Should the resolver model ever change to permit coexisting majors (a large, separate decision), this section is the trigger to revisit the hint mechanism.</fact></item>
      </list>
      <p p="32"><fact id="CONFLICT-RESOLUTION-OOS" status="spec/done">Conflict resolution (how an author forces the single chosen version) is out of scope here and documented in [`docs/faq/version-conflicts.md`](../../../docs/faq/version-conflicts.md) (`[[override]]`, git-source, `version.var`).</fact></p>
    </section>
    <section id="change-detection" title="2.7 Change-detection — a Merkle fingerprint over the boot graph">
      <p p="33"><fact id="MERKLE-FINGERPRINT" status="impl/done">**Decision.** Each unit's `STATIC.md` carries a **fingerprint** of the inputs it was compiled from — a Merkle hash over the unit's compilation zone:</fact></p>
      <fence p="34">fp(P) = hash(
    content_hash(own_boot(P)),                       // P's own boot text
    [ link_type(P→X) for each edge ],                // dynamic↔static switches
    [ fp(X) for each static / static-transitive edge P→X ],   // recurse into the zone
    [ identity(Y) = (group,name,version) for each dynamic edge P→Y ],  // dyn edge: identity only
    soft_hoist_inputs(P)                             // §2.4 global static-use counts touching P
)</fence>
      <p p="35"><fact id="fp-properties-lead" status="impl/done">Properties:</fact></p>
      <list ordered="false" p="36">
        <item><fact id="FP-DYNAMIC-BREAK" status="impl/done">**A `dynamic` edge breaks fingerprint propagation** — exactly as it breaks compilation. A change *behind* a dynamic edge changes `fp(Y)` but not `fp(P)` (only `Y`'s identity enters `fp(P)`); `Y`'s unit recompiles independently.</fact></item>
        <item><fact id="FP-STATIC-FLIP" status="impl/done">Any change **inside** a static zone — content, version, edge set, **or a `link`-type switch** (which resolution does **not** see, §2.8) — flips `fp` up the continuous static chain to the first dynamic break.</fact></item>
        <item><fact id="FP-SOFT-TRANSITION" status="impl/done">The soft-hoist term makes a **single→multi static-use transition** (a new consumer statically links `L`, so `L` must now hoist) flip `fp` for the affected units — the nonlocal invalidation soft costs, made explicit so tests target it (§3).</fact></item>
      </list>
      <p p="37"><fact id="fp-storage-note" status="impl/done">Fingerprint storage location and granularity were open here; §5 resolved both on 2026-07-15 — header storage (`RES-FP-STORAGE`) and per-package granularity (`RES-GRANULARITY`).</fact></p>
    </section>
    <section id="incremental" title="2.8 Incremental regeneration — the dirty subgraph">
      <p p="38"><fact id="DIRTY-SUBGRAPH" status="impl/done">**Decision.** Boot regeneration recompiles **only** the units whose `fp` changed (the dirty subgraph), replacing PROP-009's / PROP-011's whole-tree regeneration.</fact></p>
      <list ordered="false" p="39">
        <item><fact id="RATIONALE-CHANGED" status="impl/done">PROP-011 §2.4 kept boot regeneration whole-tree because it was cheap (a small `INDEX.md` per node); with **verbatim per-package compilation** (§2.1) that rationale no longer holds — a `STATIC.md` is now real concatenated text — so the incremental path becomes load-bearing.</fact></item>
        <item><fact id="FAST-PATH-IDEMPOTENT" status="impl/done">The fast path: an unchanged root `fp` ⇒ **zero** recompilation, **zero** git churn (idempotency).</fact></item>
      </list>
      <p p="40"><fact id="build-system-shape" status="impl/done">This is the standard build-system shape — a `cargo`-fingerprint / Bazel-action-graph dirty-subgraph. The materialisation step is already incremental (PROP-011 §2.3); this brings boot regeneration to parity.</fact></p>
    </section>
    <section id="read-set" title="2.9 Read-set — the read-time dedup">
      <p p="41"><fact id="READ-SET-DEDUP" status="impl/done">**Decision.** The `@spec`/`#use` read-set (PROP-035 §7.4 — a persistent `{ specpath, content_hash }` record, "read once") is the dedup mechanism for (a) `static-hard` duplication across units, and (b) the `#use` markers soft leaves in local units (§2.5).</fact></p>
      <list ordered="false" p="42">
        <item><fact id="READ-SET-PREREQ" status="impl/done">It is a load-bearing prerequisite, not optional: without it, `static-hard` duplicates and lifted `#use` targets would re-enter context.</fact></item>
        <item><fact id="read-set-weakness" status="impl/done">Its known weakness across context compaction (PROP-035 open question #2) applies; soft's compile-time dedup is the mitigation for the common case.</fact></item>
      </list>
    </section>
  </section>
  <section id="tests" title="3. Test obligations">
    <p p="43"><fact id="TEST-CENTRAL-RISK" status="impl/done">This system's central risk is **losing or failing to regenerate a dependency** when the graph changes. The contract:</fact></p>
    <list ordered="false" p="44">
      <item><fact id="TEST-DIFFERENTIAL-ORACLE" status="impl/done">**The differential oracle is mandatory and central.** `incremental_regen(any mutation sequence)` MUST equal `full_regen_from_scratch()`, byte-for-byte. Full regeneration is the reference semantics (it cannot silently drop anything); incremental must match it. This is the AI-Native Rust differential-oracle idiom applied to bootgen.</fact></item>
      <item><fact id="TEST-MUTATION-FUZZ" status="impl/done">**Property-based mutation fuzzing.** Generate random DAGs (packages + edges with random link modes), apply random sequences of `add-edge` / `remove-edge` / `change-link` / `bump-version` / `edit-content`, assert `incremental == full` after each. Targets the combinatorial "forgot to regenerate in a rare topology" — including the §2.7 nonlocal soft invalidation. **Shipped:** `boot/hybrid/fuzz.rs` runs the proptest sweep and names this DEF-5 in its own header.</fact></item>
      <item><fact id="TEST-GOLDEN-INVARIANTS" status="impl/done">**Invariants as characterization goldens:** *no-loss / reachability* (units reachable through `STATIC.md`+`INDEX.md` == resolved closure; nothing dropped, nothing dangling); *completeness* (every static child is compiled in; every dynamic child is a reference, not compiled); *no-stale* (recomputed `fp` == stored `fp` for every unit); *boundary isolation* (a mutation behind a dynamic edge does not change the parent unit's `STATIC.md`); *idempotency* (a no-op `vibe install` recompiles nothing, zero git diff); *dedup-at-read* (the read-set reads a duplicated/hoisted package once).</fact></item>
      <item><fact id="TEST-CHECK-INTEGRITY" status="impl/done">**`vibe check` boot-graph integrity.** The existing `vibe-check` `boot_directory` check gains a boot-graph pass: fingerprints current, reachability complete — so "did everything regenerate?" is answerable in CI and by hand.</fact></item>
    </list>
  </section>
  <section id="compat" title="4. Compatibility and migration">
    <list ordered="false" p="45">
      <item><fact id="COMPAT-EVOLVES-009" status="impl/done">**Evolves PROP-009 §2.3** — boot artifacts now generated per compilation unit, not only per entry-point node. Existing single-node projects are the degenerate case (one unit) and keep working.</fact></item>
      <item><fact id="COMPAT-RETIRES-034" status="impl/done">**Retires PROP-034 §2.2** (the global precedence lattice) — the effective-mode join is unnecessary once mode is a per-edge property resolved per unit; a package may be `static` in one unit's `STATIC.md` and `dynamic` in another's `INDEX.md` with no conflict and no global join. PROP-034's dedup / topological-order / cycle-rejection survive, applied per unit.</fact></item>
      <item><fact id="COMPAT-REVISES-011" status="impl/done">**Revises PROP-011 §2.4** — boot regeneration moves from whole-tree to dirty-subgraph (§2.8); the "boot is cheap, keep it whole-tree" decision is re-opened by the verbatim-compilation cost and its recorded trigger has fired.</fact></item>
      <item><fact id="COMPAT-DEPENDS-035" status="impl/done">**Depends on PROP-035** — the structural/JIT concepts (`#use`, read-set, link tables, reversible markers) become load-bearing rather than best-effort. This PROP is the concrete evolution of PROP-035's two-mode boot linker (§2) and its emission layer (§12).</fact></item>
      <item><fact id="COMPAT-MIGRATION-DEMO-FIRST" status="spec/done">Migration is demo-corpus-first (PROP-035 §15): build and prove on throwaway fixtures before converting any real package; vibevm itself converts last, and only where a package opts into the hybrid shape.</fact></item>
    </list>
  </section>
  <section id="open" title="5. Resolved questions">
    <p p="46"><fact id="resolved-lead" status="impl/done">The five questions opened in the design dialogue were resolved 2026-07-15 (Phase 0):</fact></p>
    <list ordered="true" p="47">
      <item><fact id="RES-ORTHOGONAL-AXES" status="impl/done">**`soft` × `static-transitive` — orthogonal axes.** `static-transitive` decides *which* packages are static (it forces the subtree); `soft`/`hard` decides *how* duplicates are deduped (hoist vs. local). They compose: a `static-transitive` edge's forced subtree is deduped by `soft` (hoisting) by default. No separate `static-transitive-hard` variant ships in v1 — the matrix stays 2×1 (`soft`/`hard`) × (`direct`/`transitive`) with hard-transitive deferred (no use case).</fact></item>
      <item><fact id="RES-USE-COUNTER" status="impl/done">**Static-use counter — both direct and forced count.** A package reached by a direct `static`/`static-hard` edge **and** a package forced static by a `static-transitive` ancestor both increment its static-use count for hoisting (§2.4). `dynamic` edges never count. This keeps hoisting correct across a forced subtree.</fact></item>
      <item><fact id="RES-FP-STORAGE" status="impl/done">**Fingerprint storage — the `STATIC.md`/`INDEX.md` header (§2.7).** A generated header comment carries the unit's `fp`, self-describing and reversible (PROP-035 §11), with **no `vibe.lock` schema bump** — avoiding an observable-contract change to the lockfile (the lighter of the RP2 options). A link-table cache (PROP-035 §10) may memoise it later; the header is the source of truth.</fact></item>
      <item><fact id="RES-GRANULARITY" status="impl/done">**Granularity — per package (v1).** Fingerprint and invalidation are per compilation unit (package). Section-level granularity (PROP-035 §5 IR) is deferred ([plan DEF-1](../../../legacy-spec/terraforms/HYBRID-LINKING-PLAN-v0.1.md)).</fact></item>
      <item><fact id="RES-DYN-BOUNDARY" status="impl/done">**Dynamic-boundary representation — aggregated into the unit's `INDEX.md`.** When a unit's static zone is compiled, every `dynamic` edge inside that zone is surfaced into the **unit's own `INDEX.md`** (not left as an inline directive in the compiled text). A unit's `INDEX.md` is thus the complete "what to load dynamically once you have read my `STATIC.md`" manifest — one manifest per unit, no inline resolution the agent must perform mid-text.</fact></item>
    </list>
    <p p="48"><fact id="MIGRATION-SAFETY-COROLLARY" status="impl/done">**Migration-safety corollary (Phase 0 finding).** Per-unit artifacts (§2.1) are **additive**: generating `STATIC.md`/`INDEX.md` inside a `vibedeps/` slot is new output, expected on migration. An entry-point node's **existing** artifacts stay **byte-identical** for a tree with no intermediate static edges (today's vibevm: `static` reaches the boot lane only through the root's `static-transitive` redbook edge, so root recursion reproduces the current root `STATIC.md`). P5's acceptance therefore checks *root artifacts unchanged* **plus** *new per-unit artifacts appear*, not "no new files".</fact></p>
  </section>
  <section id="history" title="6. Version history">
    <list ordered="false" p="49">
      <item><fact id="HISTORY-DRAFTED" status="spec/done">**2026-07-15 — drafted (owner-requested).** Captures the hybrid-linking design dialogue: per-package compilation units (§2.1); the edge as linker instruction with recursive, dynamic-bounded compilation and the `static` / `dynamic` / `static-transitive` semantics (§2.2); the `static-soft` (default) / `static-hard` modes and why soft is the default (§2.3); LCA-scoped hoisting with the within-zone/cross-zone split and hoist transitivity (§2.4); the two-ended hoist markers — local `#use` + shared-by hint (§2.5); the single-version invariant the dedup rests on and the closed multi-version-hint question (§2.6); the Merkle fingerprint over the boot graph (§2.7); dirty-subgraph incremental regeneration revising PROP-011 §2.4 (§2.8); the read-set as read-time dedup (§2.9); and the differential-oracle-centred test obligations (§3). Implementation is the [HYBRID-LINKING campaign](../../../legacy-spec/terraforms/HYBRID-LINKING-PLAN-v0.1.md).</fact></item>
      <item><fact id="HISTORY-ACCEPTED" status="spec/done">**2026-07-15 — ACCEPTED; §5 resolved (Phase 0).** Ratified by the owner's implement-in-full directive. The five open questions resolved inline (§5): soft/hard × transitive are orthogonal; both direct and forced edges increment the static-use count; the fingerprint lives in the artifact header (no lockfile bump); granularity is per-package; dynamic boundaries aggregate into the unit's `INDEX.md`. The migration-safety corollary pins per-unit artifacts as additive with entry-point artifacts byte-stable for the current tree.</fact></item>
      <item><fact id="HISTORY-IMPLEMENTED" status="spec/done">**2026-07-15 — IMPLEMENTED.** All five phases of the [HYBRID-LINKING campaign](../../../legacy-spec/terraforms/HYBRID-LINKING-PLAN-v0.1.md) landed on `main` (`d487d4e`…`381095e`), floor green throughout. In `vibe-workspace`: the per-unit recursive compiler (`boot::hybrid` — `resolve_zone` / `topo_zone`), soft hoisting (`hybrid::hoist`) with `#use` markers and shared-by hints, the `static-hard` opt-out on `LinkType`, Merkle fingerprints (`hybrid::fingerprint`) driving the emit-side dirty-subgraph skip (§2.8), and the `verify_boot_graph` integrity check (§3). Emission lives in `install/bootgen` + `bootgen/hybrid_emit`. 178 tests, specmap 0 orphans. Deferred (plan §15): broad conversion of real packages (DEF-3), a `proptest` fuzz sweep (DEF-5), and the `vibe check` CLI wiring (DEF-6). Today's tree is byte-stable — `static` reaches the lane only through the root's `static-transitive` edge, so nothing is per-unit-emitted yet.</fact></item>
    </list>
  </section>
</spec>
