<?xml version="1.0" encoding="UTF-8"?>
<spec xmlns="https://vibevm.org/spec/1">
  <title id="root">PROP-021 — Submodule sources</title>
  <status stage="impl" state="work" comment="C 2026-09-11: git-native submodules and dependency-declared immutable Git sources ship for locked skill loading; source-aware build/write roots remain"/>
  <p p="1"><fact id="status-line" status="impl/done">**Status: IMPLEMENTED** (specified 2026-06-24 in an owner-requested design
session; verified against the tree 2026-07-25 by the spec-actualization
campaign). The git backend clones with `--recurse-submodules` and runs
`submodule update --init --recursive`, snapshot embedding lands in
`git_package_registry/fetch.rs`, the in-place native form rides the PROP-022
machinery, and `resolved_commit` carries lockfile reproducibility. One of four
orthogonal specs from the bridge-packages design (siblings:
[PROP-020](../vibe-workspace/PROP-020-install-hooks.xml) install hooks,
[PROP-022](../vibe-workspace/PROP-022-materialization-modes.xml) materialization
modes, [PROP-023](PROP-023-bridge-packages.xml) bridge packages). Submodules
serve any package that wants to embed another repository — not only bridges.</fact></p>
  <p p="2"><fact id="related" status="spec/done">**Related:** [PROP-001](PROP-001-git-backend.xml) (the git backend whose clone
gains `--recurse-submodules`), [PROP-002](PROP-002-decentralized-registry.xml)
(one git repo = one package — a submodule is *not* a second package),
[PROP-010](PROP-010-local-package-cache.xml) (the live-git cache the submodule
is fetched into), [PROP-022 §2](../vibe-workspace/PROP-022-materialization-modes.xml)
(how submodule content reaches the slot differs by mode),
[PROP-003](../vibe-resolver/PROP-003-dep-evolution.xml) (the dependency grammar
  the dependency-declared form extends).</fact></p>
  <section id="motivation" title="1. Motivation">
    <section id="problem" title="1.1 The problem — embedded repos do not come along">
      <list ordered="false" p="3">
        <item><fact id="two-embed-ways" status="spec/done">A package may want to carry another repository inside it. Git offers two ways:
  a **submodule** (a reference — `.gitmodules` + a pinned gitlink commit) or a
  **vendored copy** ("git in git" — the foreign tree committed into the package).</fact></item>
        <item><fact id="vendored-works" status="spec/done">The vendored case already works: it is just files in the package tree.</fact></item>
        <item><fact id="submodule-broken" status="impl/done">The submodule case does **not** — vibevm's clone is a bare
  `git clone --branch &lt;ref&gt;` ([PROP-001](PROP-001-git-backend.xml)) with no
  `--recurse-submodules`, and the `.git`-stripping materialise copies the empty
  submodule stub. The referenced content silently never arrives.</fact></item>
      </list>
      <list ordered="false" p="4">
        <item><fact id="forcing-case-bridges" status="spec/done">The forcing case is bridge packages ([PROP-023](PROP-023-bridge-packages.xml)),
  where a maintainer submodules the upstream repo they steward.</fact></item>
        <item><fact id="mechanism-general" status="spec/done">But the mechanism
  is general: any package may legitimately embed a pinned dependency repo.</fact></item>
      </list>
    </section>
    <section id="what" title="1.2 What this is — submodule as a first-class embedded source">
      <p p="5"><fact id="WHAT-VVM-DOES" status="impl/done" action="continue" actionstage="doc" audience="author">vibevm fetches a package's submodules when it fetches the package, updates them
when it updates the package, and makes the submodule content available wherever
the package is materialised — embedded into the snapshot for the
copy-based modes, or living natively for `in-place`
([PROP-022](../vibe-workspace/PROP-022-materialization-modes.xml)).</fact></p>
    </section>
  </section>
  <section id="decisions" title="2. Decisions">
    <section id="fetch" title="2.1 Fetch and update recurse into submodules">
      <p p="6"><fact id="req-fetch" status="impl/done">`req r1`</fact></p>
      <p p="7"><fact id="RECURSE-LEAD" status="impl/done">The git backend's bootstrap and update recurse:</fact></p>
      <list ordered="false" p="8">
        <item><fact id="BOOTSTRAP-RECURSE" status="impl/done">**Bootstrap** — `git clone --recurse-submodules --branch &lt;ref&gt; -- &lt;url&gt;
  &lt;dest&gt;`. The clone lands in the live-git cache
  ([PROP-010](PROP-010-local-package-cache.xml)) with submodule working trees
  populated.</fact></item>
        <item><fact id="UPDATE-RECURSE" status="impl/done">**Update** — after the existing `fetch --prune --tags` + `reset --hard
  &lt;ref&gt;`, run `git submodule update --init --recursive` so the gitlink commits
  the new superproject ref points at are checked out. (A removed submodule is
  pruned by the reset; a moved one re-inits.)</fact></item>
      </list>
      <p p="9"><fact id="APPLIES-BOTH-CLONES" status="impl/done">This applies identically to the registry cache clone and to an `in-place`
slot clone ([PROP-022 §2.4](../vibe-workspace/PROP-022-materialization-modes.xml#in-place)).</fact></p>
      <p p="10"><fact id="PUBLISH-FLATTENS-GITLINKS" status="impl/done" action="continue" actionstage="doc" audience="author">**Publication boundary.** Workspace/direct Git source acquisition may carry
real submodules, but a per-package registry repository is a self-contained
package snapshot. Publish inspects each populated gitlink before copying,
requires its checkout to match the indexed commit and be clean, copies its
files as ordinary payload, drops `.git` and `.gitmodules`, and reports the
path plus exact vendored commit. Registry consumers therefore fetch no hidden
second repository; `resolved_commit` identifies the published package repo,
while the publication report preserves the flattened upstream evidence.</fact></p>
    </section>
    <section id="source-abstraction" title="2.2 Embedded sources — git-native and dependency-declared">
      <p p="11"><fact id="req-source-abstraction" status="impl/done">`req r1`</fact></p>
      <p p="12"><fact id="EMBEDDED-SOURCE-MODEL" status="impl/done">A submodule is modelled as an **embedded source**: content that lives at a
subpath of the package and is resolved from elsewhere. There are two
declaration forms:</fact></p>
      <list ordered="false" p="13">
        <item><fact id="FORM-GIT-NATIVE" status="impl/done">**git-native** (`.gitmodules`) — vibevm
  reads no `.gitmodules` itself; git does, via §2.1.</fact></item>
        <item><fact id="FORM-DEPENDENCY-DECLARED" status="impl/done" action="continue" actionstage="doc" audience="author">**dependency-declared** — a package's manifest uses `[[embedded_source]]`
  to name an upstream Git source independently of the package repository. The
  declaration carries a portable name, public credential-free HTTPS URL, full
  immutable commit, expected `sha256:` source-tree hash, optional ref hint, and
  upstream licence provenance. This supports snapshot/binary-distributed bridge
  packages without copying the upstream tree into their repository.</fact></item>
      </list>
      <p p="14"><fact id="NOT-A-PACKAGE" status="impl/done" action="continue" actionstage="doc" audience="author">Either way, the embedded repo is **not** a second vibevm package: it is git
content, never entered into the dependency resolver
([PROP-002](PROP-002-decentralized-registry.xml): one git repo = one package;
  the submodule is part of *this* package's content, not a node).</fact></p>
      <list ordered="false" p="15">
        <item><fact id="DECLARED-SOURCE-AUTH" status="impl/done" action="continue" actionstage="doc" audience="author">A dependency-declared source is authenticated twice: its resolved checkout
  must equal the declared commit and its deterministic source-tree hash must
  equal `content_hash`. `ref_hint` is diagnostic only and never selects bytes.</fact></item>
        <item><fact id="DECLARED-SOURCE-CACHE" status="impl/done">Authenticated trees live in an accretive content-addressed directory below
  the user's Vibe settings cache. A complete matching entry is reused offline;
  a cache miss is fetched into a sibling temporary directory and published by
  atomic rename only after both checks pass.</fact></item>
        <item><fact id="DECLARED-SOURCE-VIEW" status="impl/work">The owning package remains an ordinary physical `vibedeps` slot containing
  only its published payload. Vibe loaders resolve an explicit embedded-source
  qualifier through a root-aware view backed by the authenticated cache; there
  is no fallback from a missing package path into upstream. Generic tools may
  request the verified physical source root, but the absolute machine path is
  never persisted in the lock, slot record, index or generated artifact.</fact></item>
        <item><fact id="DECLARED-SOURCE-OPAQUE" status="impl/done">Fetch does not recurse into submodules and projection refuses gitlinks and
  symbolic links. An embedded source's own vibevm manifest, lifecycle hooks and
  dependency declarations are inert data unless a bridge explicitly selects an
  ordinary file from them.</fact></item>
        <item><fact id="DECLARED-SOURCE-WRITES" status="impl/work">The canonical cache tree is read-only input in policy. A mechanism that may
  write to its source receives a disposable copied/reflinked worktree; it never
  receives a hardlink or writable handle into the canonical cache.</fact></item>
      </list>
    </section>
    <section id="snapshot-embedding" title="2.3 Snapshot materialisation embeds the submodule content">
      <p p="16"><fact id="req-snapshot-embedding" status="impl/done">`req r1`</fact></p>
      <p p="17"><fact id="MODE-DEPENDENT" status="impl/done">How submodule content reaches the slot depends on the materialization mode
([PROP-022](../vibe-workspace/PROP-022-materialization-modes.xml)):</fact></p>
      <list ordered="false" p="18">
        <item><fact id="EMBED-SNAPSHOT" status="impl/done">**`snapshot` / `hardlink`** — the submodule's checked-out working tree is
  copied into the slot as ordinary files; nested `.git` directories and gitlink
  pointers are stripped (the same exclusion the top-level `.git` already gets).
  The submodule content is thus **vendored into the snapshot** and participates
  in the package `content_hash`.</fact></item>
        <item><fact id="EMBED-IN-PLACE" status="impl/done">**`in-place`** — nothing is copied; the submodule lives natively inside the
  slot's own git checkout, managed by git (§2.1).</fact></item>
      </list>
    </section>
    <section id="lock" title="2.4 The lockfile pins submodule state via the superproject commit">
      <p p="19"><fact id="req-lock" status="impl/done">`req r1`</fact></p>
      <list ordered="false" p="20">
        <item><fact id="LOCK-VIA-SUPERPROJECT" status="impl/done">Reproducibility rides on the package's `resolved_commit` already recorded in
  the lockfile: a superproject commit fixes the exact gitlink commit of every
  submodule, so a re-clone at `resolved_commit` with `--recurse-submodules`
  reconstructs byte-identical submodule content. No new lockfile field is
  required for the git-native form.</fact></item>
        <item><fact id="future-per-sub-pins" status="spec/done">Explicit per-submodule pins are a possible
  future refinement, tied to the dependency-declared form of §2.2.</fact></item>
      </list>
    </section>
  </section>
  <section id="rejected" title="3. Rejected alternatives">
    <list ordered="false" p="21">
      <item><fact id="REJ-RESOLVE-AS-PACKAGE" status="spec/done">**Resolving a submodule as a vibevm package** through the depsolver —
  rejected: it is git content under one package, not a registry node; treating
  it as a node would double-count identity and break PROP-002's one-repo /
  one-package rule.</fact></item>
      <item><fact id="REJ-SHALLOW-DEFAULT" status="spec/done">**Shallow submodule clones (`--depth 1`) by default** — rejected as the
  default: a shallow submodule can miss the exact gitlink commit and fail
  checkout; depth control is a possible opt-in, not the baseline.</fact></item>
      <item><fact id="REJ-PARSE-GITMODULES" status="spec/done">**vibevm parsing `.gitmodules` itself** — rejected: git already resolves
  submodules correctly across auth and nesting; reimplementing it would be a
  fragile re-do of solved work.</fact></item>
    </list>
  </section>
  <section id="out-of-scope" title="4. Out of scope">
    <list ordered="false" p="22">
      <item><fact id="OOS-DEPENDENCY-FORM" status="spec/done">**Treating a dependency-declared embedded source as a normal package
  dependency** — excluded by §2.2; it has content identity and provenance but no
  package coordinate and no resolver edges.</fact></item>
      <item><fact id="OOS-NON-GIT-SOURCES" status="spec/done">**Non-Git external source kinds** — the v1 declaration accepts only Git. An
  archive/OCI source needs its own immutable identity and extraction rules
  before it can join the same abstraction.</fact></item>
      <item><fact id="OOS-RECURSIVE-RESOLUTION" status="spec/done">**Recursive vibevm resolution inside a submodule** — a submodule's own
  `vibe.toml`, if any, is not honoured; the submodule is opaque content.</fact></item>
    </list>
  </section>
  <section id="acceptance" title="5. Acceptance">
    <list ordered="false" p="23">
      <item><fact id="ACC-CLONE-POPULATED" status="impl/done">A package whose repo declares a submodule is cloned with its submodule
  working tree populated; `update` re-checks-out submodule content for the new
  superproject ref.</fact></item>
      <item><fact id="ACC-SNAPSHOT-FILES" status="impl/done">Under `snapshot`/`hardlink`, submodule content appears in the slot as plain
  files with no nested `.git`; it contributes to `content_hash`.</fact></item>
      <item><fact id="ACC-IN-PLACE-NATIVE" status="impl/done">Under `in-place`, the submodule lives natively in the slot's git checkout.</fact></item>
      <item><fact id="ACC-LOCK-REPRODUCES" status="impl/done">Re-cloning at the lockfile's `resolved_commit` reconstructs identical
  submodule content with no extra lockfile field.</fact></item>
      <item><fact id="ACC-VENDORED-UNCHANGED" status="impl/done">A vendored ("git in git") package needs none of this — it is plain files and
  installs unchanged.</fact></item>
      <item><fact id="ACC-DECLARED-SOURCE" status="impl/done">A package distributed without `.git` can declare an exact external Git tree;
  a successful install records its source pin separately in `vibe.lock`, and a
  subsequent projection succeeds from the cache without contacting upstream.</fact></item>
      <item><fact id="ACC-FLOOR-GREEN" status="impl/done">Full `self-check.sh` green; conform 0/0/0; specmap clean.</fact></item>
    </list>
  </section>
</spec>
