Skip to content

[Feature]: Record structured provenance for installed presets and extensions #4208

Description

@nicolehaugen

Problem Statement

ExtensionRegistry and PresetRegistry currently record a hard-coded "source": "local" value for every installation, including packages installed from downloaded catalog archives. This loses the distinction between local-directory, catalog, and bundled installations.

The affected install methods are:

  • ExtensionManager.install_from_directory
  • PresetManager.install_from_directory

Both methods currently write "source": "local" when adding registry metadata. Catalog installations eventually use these same methods after downloading and extracting an archive, so catalog-installed packages are indistinguishable from packages installed from a local filesystem path.

There is no git-install pathway today. However, the registry metadata needs a forward-compatible shape so future git installation can be represented without another schema redesign.

Existing registry data may contain:

  1. No source field, from older records.
  2. "source": "local", including records created by current installs.
  3. "source": "catalog", a documented legacy value that does not identify the catalog.

Proposed Solution

  1. Define a shared internal SourceInfo schema used by both registries:
{
  "kind": "catalog | local | builtin | git",
  "catalog": "<catalog-name>",
  "path": "<absolute-path>",
  "url": "<git-url>",
  "ref": "<sha-or-ref>"
}

Field requirements:

  • kind: "local" requires an absolute path.
  • kind: "catalog" requires a catalog name for new writes.
  • kind: "builtin" has no additional required fields.
  • kind: "git" is schema-only and forward-compatible. No git producer is included in this change; future producers must provide a URL and may provide a ref.
  1. Thread an optional source value through install_from_directory in both managers and through the command-level catalog installation callers.

  2. Record the true source at call sites:

  • Bundled installations use {"kind": "builtin"}.
  • User-supplied local paths use {"kind": "local", "path": "<resolved-source-path>"}.
  • Catalog installations use {"kind": "catalog", "catalog": "<catalog-name>"}.

The default local path must be the resolved original source_dir, not the installation destination under .specify.

  1. Add shared validation and normalization helpers:
  • _validate_source(raw) validates values before they are written.
  • _normalize_source(raw) normalizes legacy values when returned from registry read APIs.
  • Registry writes store a defensive deep copy of the validated value.
  1. Normalize legacy values on read without rewriting registry files:
missing / None → {"kind": "local"}
"local"         → {"kind": "local"}
"catalog"       → {"kind": "catalog", "catalog": null}
structured dict → validate and return

{"kind": "catalog", "catalog": null} is a legacy-only read sentinel. New writes must reject it.

  1. Preserve provenance through force reinstalls, rollback, backup restoration, registry updates, and failed-install cleanup.

Alternatives Considered

  • Add separate install_from_catalog or install_from_git methods. Rejected because catalog installations already flow through install_from_directory, and no git-install pathway exists.
  • Infer provenance from the installed filesystem. Rejected because extraction removes the original source context and filesystem shape cannot reliably distinguish local and catalog installations.
  • Keep legacy strings indefinitely. Rejected because consumers would need to handle multiple incompatible representations.
  • Duplicate source validation and normalization in each registry. Rejected because the implementations could diverge.

Component

Specify CLI — initialization, commands, installation, and registry metadata.

AI Agent (if applicable)

Not applicable.

Use Cases

  1. A wizard can distinguish a preset installed from a named catalog from one installed from a local directory.
  2. Future preset list --json and extension list --json commands can expose truthful installation origin.
  3. Support and reproducibility tooling can identify how an installed package entered the project.
  4. Future git installation can use the schema without another registry migration.

Acceptance Criteria

  • ExtensionManager.install_from_directory and PresetManager.install_from_directory accept an optional structured source value.
  • Source values are validated before persistence and stored as defensive deep copies.
  • When no source is supplied, the default is {"kind": "local", "path": str(source_dir.resolve())}.
  • The default path refers to the original source directory, not the installation destination.
  • Paths are stored as absolute paths.
  • The hard-coded "source": "local" literal is removed from both install methods.
  • Bundled, local, and catalog call sites pass the correct source kind.
  • Catalog provenance is supplied by the command-level install caller that resolves the catalog entry and invokes install_from_directory; download helpers do not determine provenance.
  • A shared internal module defines SourceInfo, _validate_source, and _normalize_source.
  • Both registries import and use the shared definitions.
  • get() and list() read methods return normalized structured source values without rewriting existing registry files.
  • Missing, "local", and legacy "catalog" values normalize as specified.
  • New writes reject unknown kinds, invalid types, missing required fields, unknown fields if the schema policy disallows them, and direct use of the legacy catalog: null sentinel.
  • git is accepted as a schema-only kind, with no git-install producer required.
  • Force reinstall, backup, rollback, restore, registry update, and failed-install paths preserve or remove provenance consistently.
  • The registry schema-version decision is documented explicitly in the pull request description and changelog.
  • Existing tests and fixtures containing "source": "local" are audited and classified as either current-install fixtures or intentional legacy-normalization fixtures.
  • Tests cover local, catalog, and builtin round trips; default source-path behavior; legacy normalization; defensive-copy behavior; invalid source values; catalog installation through the command-level path; force reinstall and rollback behavior; and registry restore and update behavior.
  • Extension and preset API documentation describes the structured source schema, absolute-path behavior, legacy normalization, read-only normalization policy, and schema-only git support.

Additional Context

Blocks preset list --json and extension list --json, which are tracked separately.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementfeature-assessRun the Spec Kit idea-assessment pipeline on this feature requestfeature-goFeature assessment verdict: go — ready to hand off to /speckit.specify

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions