PROP-021 — Submodule sources
01Status: 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 install hooks,
PROP-022 materialization
modes, PROP-023 bridge packages). Submodules
serve any package that wants to embed another repository — not only bridges.
02Related: PROP-001 (the git backend whose clone
gains --recurse-submodules), PROP-002
(one git repo = one package — a submodule is not a second package),
PROP-010 (the live-git cache the submodule
is fetched into), PROP-022 §2
(how submodule content reaches the slot differs by mode),
PROP-003 (the dependency grammar
the dependency-declared form extends).
1. Motivation
1.1 The problem — embedded repos do not come along
- 03A 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). - The vendored case already works: it is just files in the package tree.
- The submodule case does not — vibevm's clone is a bare
git clone --branch <ref>(PROP-001) with no--recurse-submodules, and the.git-stripping materialise copies the empty submodule stub. The referenced content silently never arrives.
1.2 What this is — submodule as a first-class embedded source
05vibevm 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).
2. Decisions
2.1 Fetch and update recurse into submodules
06req r1
07The git backend's bootstrap and update recurse:
- 08Bootstrap —
git clone --recurse-submodules --branch <ref> -- <url> <dest>. The clone lands in the live-git cache (PROP-010) with submodule working trees populated. - Update — after the existing
fetch --prune --tags+reset --hard <ref>, rungit submodule update --init --recursiveso 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.)
09This applies identically to the registry cache clone and to an in-place
slot clone (PROP-022 §2.4).
10Publication 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.
2.2 Embedded sources — git-native and dependency-declared
11req r1
12A 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:
- 13git-native (
.gitmodules) — vibevm reads no.gitmodulesitself; git does, via §2.1. - 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, expectedsha256: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.
14Either way, the embedded repo is not a second vibevm package: it is git content, never entered into the dependency resolver (PROP-002: one git repo = one package; the submodule is part of this package's content, not a node).
- 15A 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_hintis diagnostic only and never selects bytes. - 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.
- The owning package remains an ordinary physical
vibedepsslot 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. - 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.
- 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.
2.3 Snapshot materialisation embeds the submodule content
16req r1
17How submodule content reaches the slot depends on the materialization mode (PROP-022):
- 18
snapshot/hardlink— the submodule's checked-out working tree is copied into the slot as ordinary files; nested.gitdirectories and gitlink pointers are stripped (the same exclusion the top-level.gitalready gets). The submodule content is thus vendored into the snapshot and participates in the packagecontent_hash. in-place— nothing is copied; the submodule lives natively inside the slot's own git checkout, managed by git (§2.1).
2.4 The lockfile pins submodule state via the superproject commit
19req r1
- 20Reproducibility rides on the package's
resolved_commitalready recorded in the lockfile: a superproject commit fixes the exact gitlink commit of every submodule, so a re-clone atresolved_commitwith--recurse-submodulesreconstructs byte-identical submodule content. No new lockfile field is required for the git-native form. - Explicit per-submodule pins are a possible future refinement, tied to the dependency-declared form of §2.2.
3. Rejected alternatives
- 21Resolving 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.
- 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. - vibevm parsing
.gitmodulesitself — rejected: git already resolves submodules correctly across auth and nesting; reimplementing it would be a fragile re-do of solved work.
4. Out of scope
- 22Treating 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.
- 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.
- Recursive vibevm resolution inside a submodule — a submodule's own
vibe.toml, if any, is not honoured; the submodule is opaque content.
5. Acceptance
- 23A package whose repo declares a submodule is cloned with its submodule
working tree populated;
updatere-checks-out submodule content for the new superproject ref. - Under
snapshot/hardlink, submodule content appears in the slot as plain files with no nested.git; it contributes tocontent_hash. - Under
in-place, the submodule lives natively in the slot's git checkout. - Re-cloning at the lockfile's
resolved_commitreconstructs identical submodule content with no extra lockfile field. - A vendored ("git in git") package needs none of this — it is plain files and installs unchanged.
- A package distributed without
.gitcan declare an exact external Git tree; a successful install records its source pin separately invibe.lock, and a subsequent projection succeeds from the cache without contacting upstream. - Full
self-check.shgreen; conform 0/0/0; specmap clean.