Skip to content

Add specify artifact JSON introspection for composed artifact stacks - #4300

Closed
nicolehaugen with Copilot wants to merge 3 commits into
mainfrom
copilot/feat-add-specify-artifact-command
Closed

Add specify artifact JSON introspection for composed artifact stacks#4300
nicolehaugen with Copilot wants to merge 3 commits into
mainfrom
copilot/feat-add-specify-artifact-command

Conversation

Copilot AI commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

This introduces a new specify artifact command surface to expose the effective artifact inventory and per-artifact composition stack as machine-readable JSON. It closes the gap between resolver behavior and external tooling by making layer provenance, active/hidden state, and lookup IDs queryable.

  • CLI surface

    • Added specify artifact command group registration at the root CLI.
    • Added:
      • specify artifact list --json → flat inventory (id, name, kind, description)
      • specify artifact info <name> --json → resolved artifact + ordered stack metadata
  • Artifact catalog + stack model

    • Added a dedicated artifact catalog module that:
      • Enumerates core + resolver-visible contributions
      • Resolves ambiguity across command / template / script
      • Emits deterministic stack rows with:
        • layer, presetId, presetName, strategy
        • active, hidden, manifestPath, lookupId
    • Added structured JSON error envelopes for unknown/ambiguous/not-project/resolution-failure cases.
  • Deterministic IDs and lookup parity

    • Introduced shared identifier helpers for stable contribution and stack lookup IDs.
    • Extended preset manifest/resolver integration so stack layers carry lookupId consistently across project/preset/extension/core layers.
  • Script runtime variant normalization

    • Added shared script-variant utilities to normalize logical script names across .sh, .ps1, .py (including Python underscore→hyphen mapping).
    • Updated resolver and artifact inventory paths to treat script variants consistently for:
      • core assets
      • convention-based preset/extension artifacts
      • project override artifacts
  • Behavioral contract coverage

    • Added focused artifact command tests for:
      • JSON schema and stderr error envelope contracts
      • ambiguity and kind-disambiguation behavior
      • composition stack semantics (active/hidden)
      • path portability (manifestPath posix/relative)
      • resolver parity and script variant handling
specify artifact list --json
specify artifact info speckit.constitution --json
specify artifact info command:speckit.constitution --json

Copilot AI balanced review requested due to automatic review settings August 24, 2026 16:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review any files in this pull request.

Co-authored-by: nicolehaugen <10600161+nicolehaugen@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 24, 2026 17:04
Co-authored-by: nicolehaugen <10600161+nicolehaugen@users.noreply.github.com>
Copilot AI changed the title [WIP] Add specify artifact command exposing composition stacks as JSON Add specify artifact JSON introspection for composed artifact stacks Aug 24, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Suppressed comments (1)

src/specify_cli/artifacts/_commands.py:148

  • The info path has the same JSON-envelope gap as list: catalog directory reads can raise OSError, which bypasses both handlers and produces a traceback/non-JSON stderr response. Map it to ArtifactResolutionError here too.
    except PresetError:
        _emit_error_and_exit(ArtifactResolutionError())
        return  # pragma: no cover
  • Files reviewed: 8/8 changed files
  • Comments generated: 4
  • Review effort level: Balanced

Comment on lines +655 to +658
def _get_baseline(self) -> CoreBaseline:
if self._baseline is None:
self._baseline = CoreBaseline.load()
return self._baseline
Comment on lines +62 to +66
manifest = {
"id": pack_id,
"version": "1.0.0",
"metadata": {"name": f"Test preset {pack_id}"},
"provides": provides,
Comment on lines +23 to +27
manifest = {
"id": pack_id,
"version": "1.0.0",
"metadata": {"name": f"Test preset {pack_id}"},
"provides": provides,
Comment on lines +104 to +106
except PresetError:
_emit_error_and_exit(ArtifactResolutionError())
return # pragma: no cover — _emit_error_and_exit raises
Copilot AI review requested due to automatic review settings August 24, 2026 17:10
Copilot AI requested a review from nicolehaugen August 24, 2026 17:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

src/specify_cli/artifacts/init.py:365

  • An enclosing manifest is not necessarily the manifest that declared this layer. When a valid preset or extension manifest omits an artifact and the resolver uses convention fallback, this walk still returns that manifest as manifestPath, falsely attributing the contribution to it. Carry declaration provenance from the resolver or verify that the manifest contains the matching kind/name before exposing its path.
    manifest = _find_enclosing_manifest(source)

src/specify_cli/artifacts/init.py:607

  • Descriptions do not follow the effective resolver layer. Core rows are inserted first, so this condition preserves a non-empty core description even when a preset actively replaces that artifact; for non-core collisions, alphabetic directory order wins instead of preset/extension priority. Select the description associated with the highest active resolver layer so the flat inventory represents the effective artifact.
            elif description and not seen[key].description:
  • Files reviewed: 8/8 changed files
  • Comments generated: 3
  • Review effort level: Balanced

for entry in sorted(overrides_dir.iterdir(), key=lambda p: p.name):
if not entry.is_file() or entry.suffix != _TEMPLATE_SUFFIX:
continue
name = entry.stem
Comment on lines +780 to +782
for entry in sorted(scripts_dir.iterdir(), key=lambda p: p.name):
if entry.is_file() and entry.suffix in (".sh", ".ps1", ".py"):
name = canonical_script_name(entry)
Comment on lines +818 to +824
if entry.suffix not in (".sh", ".ps1", ".py"):
continue
name = canonical_script_name(entry)
if name is None:
name = entry.stem.replace("_", "-") if entry.suffix == ".py" else entry.stem
if ":" not in name:
yield kind, name
@nicolehaugen

Copy link
Copy Markdown

This is being closed because the issues are being addressed in PR #4267

@nicolehaugen
nicolehaugen deleted the copilot/feat-add-specify-artifact-command branch August 24, 2026 17:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants