You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
resolve against current installed manifests on every request, without a cache or contribution index
Design
#4305 introduced artifact inventory and composition stacks with deterministic, artifact-owned lookupId values. #4348 extended that contract to native additive hook artifacts. This PR completes the Wizard-facing round trip within the artifact API:
specify artifact list --json returns a non-built-in stack layer with lookupId.
A consumer passes that opaque value to specify artifact lookup <lookupId> --json.
The CLI returns the provider coordinates, installed manifest and source paths, and the validated declaration Spec Kit uses under contribution.
The implementation intentionally does not add lookupId fields to PresetResolver.collect_all_layers() or preset/extension manifest and info representations. The artifact command remains the single public boundary for this cross-reference, avoiding changes to existing preset and extension APIs.
Provenance and identity
Here, provenance means the origin of one artifact layer: the installed preset or extension that supplied it, the manifest that declared it, and the concrete file that backs it.
Lookup IDs use the installed preset or extension ID—the registry/directory identity already used by Spec Kit’s resolver ordering. This keeps separate installed directories distinct even when their manifests declare the same logical ID, while preserving the existing named-layer precedence and additive hook behavior. Hook activation remains keyed to the manifest ID stored by the existing hook runtime. The lookup ID is opaque to consumers.
Lookup behavior
Manifest-backed preset and extension commands, templates, and scripts resolve to their validated, normalized declaration.
contribution reflects normal Spec Kit manifest processing, including canonical extension names, injected defaults, and normalized preset strategies; it is not a verbatim copy of the authored YAML.
Duplicate manifest IDs remain separate resolver layers and receive distinct lookup IDs through their installed IDs.
sourcePath is returned only when the declared path exists and safely resolves beneath the installed provider directory, including when the project root has a symlinked ancestor.
Built-in layers have no lookupId.
Project and convention-only layers have no originating manifest declaration and return unknown contribution.
Invalid or missing lookup IDs return the standard artifact JSON error envelope with exit code 1.
Contribution values that cannot be represented as strict UTF-8 JSON return the standard artifact resolution failed error envelope.
Scope
Production changes are confined to src/specify_cli/artifacts/. No preset, extension, resolver, registrar, integration, or hook-runtime behavior is changed.
The success test never checks the new manifestPath or sourcePath fields. As a result, the containment/existence logic in _contribution_result could return an unsafe path—or fail to return the valid authored file—without failing this suite. Add assertions for the valid preset path plus cases where a missing or escaping declared file yields sourcePath: null.
This is not always the original authored mapping. ExtensionManifest rewrites supported legacy command names and hook references in memory and inserts default aliases, while preset loading normalizes an authored strategy. Since lookup returns those validated in-memory entries, document the payload as normalized/validated unless raw YAML is preserved separately.
contribution contains the raw manifest mapping, whose validators allow unknown fields and therefore JSON-incompatible YAML values such as timestamps, binary values, or sets. json.dumps then raises TypeError here outside the error handler, so a valid installed manifest can make this JSON-only command fail without the standard artifact error envelope. Define and apply a stable JSON-safe normalization or validation policy before serialization.
This copies the validated manifest entry, but both manifest parsers mutate declarations in place: ExtensionManifest rewrites legacy command/hook names and injects aliases: [] (src/specify_cli/extensions/__init__.py:473-535), while PresetManifest lowercases authored strategies (src/specify_cli/presets/__init__.py:487-497). Consequently contribution is not the exact/original authored declaration promised by this API. Preserve a raw declaration for the response while continuing to use the normalized representation for resolver matching, and cover at least a legacy extension name and mixed-case preset strategy.
Keep hook activation keyed to the runtime extension ID
src/specify_cli/artifacts/catalog.py:639
Changing source_id to the installed directory ID also changes the later active comparison, but HookExecutor.register_hooks() writes manifest.id into each runtime binding (src/specify_cli/extensions/__init__.py:5008-5078). For a supported renamed provider (extension_id != manifest.id), its registered hook is therefore reported inactive even though the runtime returns an enabled binding. Keep the installed ID for lookupId/provenance, but carry the manifest ID separately when matching runtime bindings, with a mismatch regression test.
Do not resolve away the project-relative source path
src/specify_cli/artifacts/catalog.py:557
Passing the resolved candidate into _repo_relative_existing_file() makes a valid source disappear when the project path has a symlinked ancestor: the candidate is physical while project_root remains lexical, so relative_to(project_root) fails. This contradicts the existing resolver pattern at src/specify_cli/presets/__init__.py:5457-5466, which resolves only for containment and returns the unresolved candidate. Keep the unresolved path for reporting after checking its resolved form, and add a symlinked-root regression case.
Preserve runtime hook identity separately from lookup identity
src/specify_cli/artifacts/catalog.py:639
Using the installed directory ID here also changes activation matching below: stack entries compare binding["extension"] with this value, but HookExecutor.register_hooks() writes the manifest's logical ID (src/specify_cli/extensions/__init__.py:5077). For a supported renamed installation where extension_id != manifest.id, the runtime will return and execute the binding while artifact inventory reports the hook as inactive/unregistered. Keep the installed ID for sourceId/lookupId, but carry the manifest ID separately for runtime-binding matching, and cover this mismatch with a registration-state regression test.
Review-round summary for commits \�469f97f\ and \7b9ca4c1:
Defined \contribution\ as the validated, normalized declaration Spec Kit uses, matching the existing preset/extension list pattern; regressions cover legacy extension-name/default normalization and mixed-case preset strategies.
Preserved installed provider identity for \sourceId/\lookupId\ while matching hook activation with the runtime \manifest.id; a renamed-installation regression verifies the hook remains active.
Preserved the lexical project-relative source path after resolved-path containment checks; a symlinked-project-root regression covers the prior null result.
Reject contributions that cannot be encoded as UTF-8 JSON
src/specify_cli/artifacts/catalog.py:441
This guard still accepts unpaired Unicode surrogates because json.dumps defaults to ensure_ascii=True. A YAML contribution can contain an escaped surrogate in an optional field, after which the final ensure_ascii=False serialization reaches sys.stdout.write and raises UnicodeEncodeError instead of returning the documented artifact resolution failed envelope. Validate the same non-ASCII representation that the CLI emits (and add this case to the non-JSON-value regression test).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
specify artifact lookup <lookupId> --jsonto resolve a manifest-backed artifact stack layer to its effective contribution declarationspecify_cli.artifactsDesign
#4305 introduced artifact inventory and composition stacks with deterministic, artifact-owned
lookupIdvalues. #4348 extended that contract to native additive hook artifacts. This PR completes the Wizard-facing round trip within the artifact API:specify artifact list --jsonreturns a non-built-in stack layer withlookupId.specify artifact lookup <lookupId> --json.contribution.The implementation intentionally does not add
lookupIdfields toPresetResolver.collect_all_layers()or preset/extension manifest and info representations. The artifact command remains the single public boundary for this cross-reference, avoiding changes to existing preset and extension APIs.Provenance and identity
Here, provenance means the origin of one artifact layer: the installed preset or extension that supplied it, the manifest that declared it, and the concrete file that backs it.
Lookup IDs use the installed preset or extension ID—the registry/directory identity already used by Spec Kit’s resolver ordering. This keeps separate installed directories distinct even when their manifests declare the same logical ID, while preserving the existing named-layer precedence and additive hook behavior. Hook activation remains keyed to the manifest ID stored by the existing hook runtime. The lookup ID is opaque to consumers.
Lookup behavior
contributionreflects normal Spec Kit manifest processing, including canonical extension names, injected defaults, and normalized preset strategies; it is not a verbatim copy of the authored YAML.sourcePathis returned only when the declared path exists and safely resolves beneath the installed provider directory, including when the project root has a symlinked ancestor.lookupId.unknown contribution.artifact resolution failederror envelope.Scope
Production changes are confined to
src/specify_cli/artifacts/. No preset, extension, resolver, registrar, integration, or hook-runtime behavior is changed.Validation
PYTHONPATH=src python -m pytest -q tests/test_artifact_command.py tests/test_artifact_command_parity.pygit diff --checkpassedUpdated on behalf of @nicolehaugen by GitHub Copilot (model: GPT-5.6 Sol, autonomous).