Bundle Spec v0.1.1

Sibling format to Capsule. A Bundle is a portable, self-describing archive of related files — viewers, data, and assets — designed to be shared, hosted, or stored as a single unit when a single sealed HTML file is not the right shape for the artifact.

Bundle is the second format in the htmlcapsule family. It emerged from real producer pressure (the strata leak investigation; see F31 in RESEARCH.md) when a working artifact carried too much heavy data — large georeferenced rasters, LiDAR point clouds, multiple viewer HTMLs — to fit comfortably inside a Capsule's sealed-singleton commitment.

Bundle borrows three principles from the Capsule spec:

  1. Identity — every bundle has a UUID, minted by the author at seal time
  2. Integrity — every file has a SHA-256 hash recorded in the manifest
  3. Provenance — the manifest records who created it, when, and from what

Where a Capsule is a single sealed HTML file with everything inlined and no network, a Bundle is a directory of files with a manifest at root that may carry declared external dependencies. Bundles are for projects that exceed what a single file can reasonably contain — heavy assets, multiple viewers, binary data formats, working substrates that will eventually have sealed Capsule reports derived from them.

A Bundle is not a relaxed Capsule. It is a sibling format with a different boundary. If the artifact fits in one offline HTML file, publish a Capsule. If the artifact needs a directory of files, multiple viewers, heavy binary assets, or declared external libraries/resources with primary artifact data local, publish a Bundle. The distinction is semantic, not just packaging: a Capsule's promise is "the whole thing is in this one HTML file"; a Bundle's promise is "the whole project is in this manifest-described set of files."


1. What makes a valid Bundle

A Bundle is a zip archive (or directory) containing:

  1. A manifest.json at the root
  2. At least one entry HTML file declared in the manifest
  3. All payload files listed in manifest.files[]
  4. All local files referenced by the entry HTML(s), CSS, or other viewers as relative paths inside the bundle

That's the validity floor. Everything else in this spec is convention until real producers and hosts make it load-bearing.

1.1 Quick choice rule

Use a Capsule when the deliverable is a sealed document or small interactive archive that can stand alone as one .html file with no network dependency.

Use a Bundle when the artifact is a project-shaped object: multiple files, heavy assets, multiple viewer entry points, binary data formats, or declared external libraries.

If you're unsure, start with Capsule. Move to Bundle only when Capsule's sealed-singleton boundary would force you to lie: external assets, hidden sidecar files, a 100 MB HTML file, or multiple viewers pretending to be one page.

1.2 What Bundle inherits from Capsule

Bundle inherits the htmlcapsule family's discipline, not Capsule's exact mechanics.

Inherited at the family level:

Not inherited from Capsule:

A Bundle entry viewer may itself be a valid Capsule, but it does not become one merely by living inside a Bundle. Conversely, a Bundle can contain ordinary HTML viewers that are not Capsules, as long as the Bundle manifest honestly inventories the files and dependencies.

2. The manifest

2.1 Required fields

FieldTypeDescription
bundle_versionstringSpec version. Currently "0.1.1"
uuidstringUUID v4, minted by the author at seal time
titlestringHuman-readable title
entrystringRelative path to the primary entry HTML. Must point inside the bundle and appear in files[]
filesarrayComplete payload-file inventory (see §2.3). Excludes manifest.json itself
FieldTypeDescription
descriptionstringOne-paragraph summary of the bundle
revisionstringOptional human-facing instance/revision label. This is the Bundle's content revision; bundle_version is the spec version
created_atstringISO 8601 timestamp
sealed_atstringISO 8601 timestamp of when hashes were computed
created_byobjectAuthor identification (flexible shape)
domainstringSubject domain (e.g. "building_investigation", "design_system")
bundle_profilestringTop-level shape of the manifested file set. Recommended values: viewer, data_package, multi_entry, project_archive
entriesobjectAdditional entry points beyond the primary one
integrityobjectHash algorithm and scope declaration
external_dependenciesarrayCDN libraries or other runtime network deps
parentsarrayUUIDs of parent Capsules or Bundles this was forked or derived from
derived_fromarrayNon-Capsule / non-Bundle sources (same shape as Capsule's derived_from[]; see CAPSULE_SPEC.md §11.2)

2.2.1 Bundle profiles

Capsule uses profile to describe how one fixed HTML envelope is used. Bundle uses bundle_profile to describe the shape of the manifested file set. The field answers a different question: what kind of project-shaped object is this?

bundle_profile is recommended, not required, in Bundle v0.1.1. If absent, consumers should treat the object as an unprofiled legacy Bundle and apply the base rules.

Initial values:

ValueMeaningTypical shape
viewerThe primary experience is an HTML viewer over inventoried local files.One entry HTML file, local data/assets in files[], optional declared external libraries
data_packageThe primary artifact is the manifest-described dataset/file set; the viewer is explanatory or optional.Data-first inventory with role: "data" / domain roles; entry page summarizes the package
multi_entryThe Bundle has multiple meaningful entry points.Primary entry plus named entries for alternate viewers, reports, or modes
project_archiveThe Bundle preserves a bounded working/output folder with provenance.Source assets, plans, scripts, generated outputs, and one or more viewers/reports

Do not use bundle_profile as a substitute for domain. domain says what subject area the Bundle is about; bundle_profile says what container pattern the Bundle uses. Do not use it as an interactivity tier either. Whether a viewer is mostly static or highly interactive is an implementation detail inside the entry HTML; the Bundle-level profile is about the file set.

Adoption plan

  1. v0.1.1 establishes bundle_profile as recommended manifest vocabulary and adds validator recognition.
  2. Existing v0.1.0 Bundles remain valid; missing bundle_profile is a warning, not a failure.
  3. Profile-specific heuristics should stay conservative until real producers need them. For now, the validator checks only that a declared profile is one of the known values.

2.3 File inventory

Each item in the files array describes one payload file. The manifest file is not listed in files[], because it cannot contain a stable hash of itself without a second hashing protocol.

FieldTypeRequiredDescription
pathstringyesRelative path from bundle root
sha256stringyesSHA-256 hex digest
sizeintegeryesSize in bytes
rolestringrecommendedOne of: entry, data, metadata, plan, raster, pointcloud, asset
formatstringrecommendedFile format: html, geojson, json, jpeg, png, xyz_rgb_binary, etc.
descriptionstringoptionalHuman-readable description

Additional fields (like floor, pixels, pixel_scale_m) are allowed and encouraged when they help a consumer understand the file without opening it.

Path rules:

2.4 External dependencies

Bundles may reference external libraries or runtime resources needed by a viewer. They must be declared so a recipient can tell whether the bundle will work offline, partially offline, or only with network access.

Recommended shape:

"external_dependencies": [
  {
    "url": "https://unpkg.com/[email protected]/dist/leaflet.js",
    "kind": "script",
    "purpose": "2D map viewer",
    "required": true
  },
  {
    "url": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js",
    "kind": "script",
    "purpose": "3D point-cloud viewer",
    "required": true
  }
]

String entries are also accepted for simple manifests:

"external_dependencies": [
  "https://unpkg.com/[email protected]/dist/leaflet.js"
]

The declaration is not permission to hide live state outside the bundle. A CDN library, basemap service, or rendering helper can be an external dependency; the point cloud, GeoJSON, rasters, notes, reports, and other artifact substance belong inside the bundle.

If the entry viewer fetches the artifact's primary evidence or source data from a live API at view time, the object is no longer a sealed Bundle. It is a connected project or hosted application with a manifest. Useful, but a different category.

2.5 Minimal manifest example

{
  "bundle_version": "0.1.1",
  "uuid": "4c64bd22-9573-4b47-9a6f-9f7a685e86a1",
  "title": "Example Investigation Bundle",
  "revision": "1.0.0",
  "description": "A small public example showing one viewer, one stylesheet, and one data file.",
  "created_at": "2026-05-24T00:00:00Z",
  "sealed_at": "2026-05-24T00:00:00Z",
  "created_by": {
    "name": "htmlcapsule reference example",
    "kind": "human"
  },
  "domain": "example",
  "bundle_profile": "viewer",
  "entry": "viewer/index.html",
  "files": [
    {
      "path": "viewer/index.html",
      "sha256": "sha256:...",
      "size": 1234,
      "role": "entry",
      "format": "html"
    },
    {
      "path": "data/summary.json",
      "sha256": "sha256:...",
      "size": 456,
      "role": "data",
      "format": "json"
    }
  ],
  "external_dependencies": []
}

2.6 Domain-specific extensions

The manifest can include domain-specific blocks at the top level, similar to Capsule's domain extensions in DOMAIN_CAPSULES.md. For spatial bundles, a spatial block is recommended:

{
  "spatial": {
    "crs": "EPSG:26910",
    "bounds_utm": { "west": 491663, "east": 491701, "south": 5457008, "north": 5457061 },
    "bounds_wgs84": { "west": -123.1144, "east": -123.1141, "south": 49.2659, "north": 49.2662 }
  }
}

For investigation bundles, a data_summary block:

{
  "data_summary": {
    "features_total": 83,
    "floors": [5, 6, 7, 8],
    "categories": ["roof_deck_drain", "planter_drain", "leak"]
  }
}

The spec does not prescribe these shapes — they're conventions that emerge per domain, the same way Capsule's domain schemas earn their slot when a real producer ships a domain capsule.

2.7 Lineage fields

Bundle uses two lineage fields, mirroring Capsule's split between hard family provenance and broader source provenance:

This differs slightly from Capsule. Capsule parents[] is strict Capsule-to-Capsule lineage; when a Capsule is derived from a Bundle, the Bundle belongs in the Capsule's derived_from[] array with type: "bundle" rather than in parents[].

3. The boundary

A Capsule boundary is the HTML file. A Bundle boundary is the root directory plus manifest.json.

3.1 What's inside the bundle

3.2 What's outside the bundle

Unlike a Capsule, a Bundle is allowed to have external dependencies. The manifest SHOULD declare them in external_dependencies so a consumer knows what network access is needed. A Bundle without external dependencies is strictly better (works offline), but the spec does not require it.

This is a load-bearing difference between the two formats, but it should not be misread as "Bundle can put the real artifact somewhere else." Capsule's Rule 2 (CAPSULE_CORE.md) elevates "no network" to a definitional boundary — an artifact that depends on external services is a different category, not a degraded Capsule. Bundle relaxes the no-network commitment in exchange for handling heavy artifacts and multi-viewer setups that Capsule's ~20 MB practical ceiling can't realistically contain. It does not relax the provenance and inventory commitment: the files that make the artifact what it is belong in files[].

3.3 Boundary anti-patterns

These are signs the artifact is neither a good Capsule nor a good Bundle yet:

3.4 Capsule vs. Bundle

CapsuleBundle
ContainerSingle .html fileZip archive / directory
Boundary promiseEverything needed is in the HTML fileEverything local is in the manifest-described file set; external deps are declared
NetworkNo external requestsDeclared external libraries/resources allowed; primary artifact data stays local
SizePractical limit ~10–20 MBNo hard limit
DataInline JSON blockSeparate files, any format
ViewersBuilt into the HTMLSeparate HTML files
Use caseSealed deliverables, reports, small interactive archivesWorking projects, heavy data, multi-viewer artifacts
OfflineAlways works offlineWorks offline if no CDN deps
IntegrityOptional/required content hash depending on producer kindPer-file SHA-256 hashes in manifest
Best mental modelPortable documentPortable project
SpecCAPSULE_SPEC.mdThis document

Composition. A Capsule can be derived from a Bundle: a compiler reads the bundle manifest, extracts a bounded view of the data, and emits a sealed HTML report. Because Capsule parents[] is strict Capsule-to-Capsule lineage, a source Bundle belongs in the Capsule manifest's derived_from[] array, for example:

"derived_from": [
  {
    "type": "bundle",
    "title": "Loft 495 leak investigation bundle",
    "reference": "urn:uuid:4c64bd22-9573-4b47-9a6f-9f7a685e86a1",
    "role": "source project"
  }
]

The reverse relationship is usually containment, not conversion: a Bundle may include one or more Capsules as sealed reports inside its files[] inventory, alongside the heavier data and viewers that produced them. Those report files remain independently valid Capsules; the Bundle manifest simply accounts for their file integrity and places them in a project-shaped context.

Choosing between them. A producer reaches for:

4. Packaging

4.1 Zip format

The canonical distribution format is a zip archive. The manifest MUST be at the zip root (not inside a subdirectory).

my-project.zip
├── manifest.json       ← root level
├── viewer/
│   └── index.html
└── data/
    └── features.geojson

Not this:

my-project.zip
└── my-project/         ← extra nesting breaks relative paths
    ├── manifest.json
    └── ...

4.2 Naming

The zip filename is not standardized. The uuid in the manifest is the canonical identifier.

4.3 Compression

Standard zip deflate. No special requirements.

4.4 Directory form

During local development, a Bundle may be an ordinary directory. The same root rules apply: manifest.json at the directory root, paths relative to that root, and no file references that escape the directory. Zip is the distribution form; directory is the authoring and verification form.

5. Hosting

A host that receives a bundle (Stratabot is the canonical first host; see HOSTING.md for the equivalent host-contract pattern Capsule hosts follow):

  1. Reads manifest.json to get the UUID, title, entry path
  2. Stores files under a prefix keyed by UUID
  3. Serves the entry HTML in an iframe or at a route
  4. Serves sibling files at relative paths so the viewer works unchanged
  5. Optionally verifies SHA-256 hashes against the manifest
  6. Optionally returns response headers attesting hash and UUID (analogous to x-capsule-content-hash / x-capsule-uuid for Capsules)

The sharing layer (ACLs, share tokens, access logs) attaches to the bundle UUID, not to individual files.

Multi-format hosts. A host like Stratabot may accept both Capsules and Bundles, dispatching by file type or manifest shape. That is a desirable property — a domain-aware host should serve whatever sealed/manifested artifact is the right shape for the deliverable, not require producers to choose a host based on format.

6. Integrity verification

To verify a Bundle's integrity, check every file listed in manifest.files[] against its declared size and SHA-256 hash:

import json, hashlib

manifest = json.load(open("manifest.json"))
for f in manifest["files"]:
    actual = hashlib.sha256(open(f["path"], "rb").read()).hexdigest()
    assert actual == f["sha256"], f"MISMATCH: {f['path']}"
print("All files verified.")

The manifest itself is not self-hashing (it can't contain its own hash). A host MAY compute and store a hash of the manifest separately for tamper detection.

A reference validator lives at ../compiler/validate_bundle.py. It accepts either a bundle directory or a zip archive:

python3 compiler/validate_bundle.py spec/examples/minimal_bundle
python3 compiler/validate_bundle.py /tmp/example.bundle.zip

7. Versioning

bundle_version is the version of this specification, not the version of one Bundle instance. If a producer needs a human-facing instance version, use the optional revision field. The validity floor remains sealed_at plus the per-file hashes.

When a bundle is updated (new features added, plans revised), the author SHOULD:

  1. Update sealed_at to the new seal time
  2. Recompute all file hashes
  3. Keep the same uuid (it's the same project)
  4. Increment revision if recipients need a human-facing version label

If the update is a true fork rather than a revision of the same project, mint a new UUID and record the prior Bundle in parents[]. The previous sealed state can also be preserved by keeping the old zip. The manifest does not currently support internal version history — that's a future extension if real producer pressure surfaces.


8. The producer / format / host pattern

Bundle's emergence makes a project-level pattern explicit that was previously implicit in Capsule's design:

The host stays light. The producer can be heavy and domain-specific. The portable format is the contract that lets them compose.

A producer (a domain-specific tool, a side project, an investigation environment) does the heavy work in whatever stack fits the domain — geospatial, music, photo, document, anything. It emits a sealed/manifested artifact in a portable format. A host receives the artifact and serves it without needing to know what tooling produced it.

The pattern instantiates as:

Both follow the same shape. Future producers and hosts can be added to either family without changing the format. New sibling formats can emerge when a domain's empirical pressure pushes past both Capsule and Bundle (this is unlikely in the near term but the spec is structured to allow it).


This spec is v0.1.1. It will evolve as real bundles get shared and hosted. The goal is to stay minimal: a manifest, file hashes, profile vocabulary, and an entry point. Everything else is convention.

Bundle joined the htmlcapsule family in project v0.4.0 (2026-05-24). See F31 in RESEARCH.md for the empirical pressure and trajectory that produced it.

About this page · manifest · exports

This is a sealed HTML Capsule per Core spec v0.3.0. Five required inline blocks, no network dependencies, integrity hash over data + manifest. The content above is rendered from spec/BUNDLE_SPEC.md by the deterministic compiler/build_md_capsules.py at the time of the last source change.

Loading manifest…