-
Notifications
You must be signed in to change notification settings - Fork 11.8k
feat: add deterministic contribution IDs and specify artifact command
#4305
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
nicolehaugen
wants to merge
86
commits into
main
Choose a base branch
from
nicolehaugen-contribution-ids-and-artifacts
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
86 commits
Select commit
Hold shift + click to select a range
3807522
Add deterministic contribution IDs and stack lookup IDs for resolved …
nicolehaugen 9a441fe
feat: add `specify artifact` command exposing composition stacks as JSON
nicolehaugen c47371e
Potential fix for pull request finding 'Module is imported with 'impo…
nicolehaugen ea2636f
Potential fix for pull request finding 'Module is imported with 'impo…
nicolehaugen b1d6a75
Potential fix for pull request finding 'Unused import'
nicolehaugen 424ca0f
Project preset artifacts by entry type
Copilot ac90181
Represent project override artifact layers
Copilot ed2f3db
Preserve artifact JSON init-dir errors
Copilot 7eb1a52
Canonicalize core script artifacts
Copilot 70965d7
Potential fix for pull request finding
nicolehaugen 70a937e
Fix artifact inventory resolver filtering
Copilot df4afef
Add resolver tests for single-runtime core scripts
Copilot 028fcd8
Cache artifact resolver lookups
Copilot 8e7cf83
Handle artifact resolver failures
Copilot fcb72b1
Document artifact resolution error
Copilot 69baa87
Include convention-based artifacts in inventory
Copilot e4347b0
Restore legacy flat core script lookup
Copilot 2c36d57
Extend convention discovery to presets in artifact inventory
Copilot f6eacd2
Fix manifest path portability and export ArtifactResolutionError
Copilot f601efa
Bound artifact manifest search to project root
Copilot 5410f72
Cover project-root artifact manifests
Copilot 9b68bac
Handle directory artifact manifest lookups
Copilot f9ee35c
Fall back to top-level preset name in artifact stacks
Copilot 95bbd99
Include project-local core artifacts in inventory
Copilot aacb49b
Address inline review feedback on artifact resolver helpers
Copilot fef72da
Reuse manifest/registry APIs in artifact contribution enumeration
Copilot 442cd23
Pass layer explicitly to _iter_pack_contributions instead of inferrin…
Copilot ddadd5f
Fix core command namespacing and validate names for kind-scoped lookups
Copilot 602f042
Skip manifest contributions without a usable identifier
Copilot 63237cf
Hoist test-local imports to module scope in artifact/assets tests
Copilot 67c107f
fix: resolve artifact inventory and validation review regressions
Copilot 2514302
perf: avoid duplicate read in core command inventory
Copilot f93f526
fix: classify dotted override-only artifacts as commands
Copilot 5b08932
fix: accept single-segment artifact commands
Copilot 355ed82
fix: fail closed on corrupt artifact registries
Copilot d0f9f25
fix: trust inventory for artifact info lookups
Copilot b4de317
fix: validate registry before artifact info
Copilot 4857201
fix: resolve artifact description by layer precedence, not enumeratio…
Copilot a20502d
fix: validate subdir before wheel bundle lookup in _locate_core_asset…
Copilot a149714
fix: detect duplicate hooks after command canonicalization
Copilot 9fd43d7
fix: reuse normalized hook entries for duplicate detection
Copilot 4682021
fix: align core command candidate ordering
Copilot c816a29
test: cover manifest-backed artifact parity
Copilot 60050a8
fix: align artifact IDs with resolver identity
Copilot 1c4842f
fix: skip invalid local artifact name components
Copilot a3293eb
fix: filter invalid local artifact IDs from inventory
Copilot 0377861
fix: align artifact preset enumeration with resolver
Copilot 1739a09
test: remove tautological artifact tests and strengthen id assertion
Copilot 866b3a5
fix: preserve documented hook duplicate semantics
Copilot 467ddca
fix: dedupe hook contributions last-wins
Copilot a2fc586
docs: clarify hook identifier deduplication
Copilot f9efe06
docs: remove hook discriminator references
Copilot 9ae2682
style: space identifier declarations
Copilot 7e0e7dc
fix: address unresolved review feedback on PR #4305
Copilot e1a63ce
fix: keep on-disk preset/extension identity separate from lookupId
Copilot fd2b268
fix: remove stale lookupId parsing fallback and tighten malformed loo…
Copilot 1c4319c
fix: route resolver core fallback through shared asset resolver, desc…
Copilot f7b549f
refactor: drop redundant derive_named_id import-visibility assignment
Copilot 083af4c
fix: align artifact inventory and lookup ID validation
nicolehaugen bd77448
fix: fail closed on malformed artifact registries
nicolehaugen 46394a9
Merge remote-tracking branch 'origin/nicolehaugen-contribution-ids-an…
nicolehaugen 436f394
fix: preserve convention artifact descriptions
nicolehaugen 7416247
fix: align artifact override resolution
Copilot 4b8b381
Refactor artifact inventory candidates
Copilot 19beb04
Address artifact inventory review
Copilot dd9ef64
Address artifact inventory review
Copilot 8c29bfe
Address artifact inventory review
Copilot 8678014
source-agnostic artifact IDs; built-in tier recognized by exclusion, …
Copilot 05eaf72
fix: reject malformed artifact layer provenance
Copilot b6f4481
Tighten artifact provenance handling
Copilot cea47c7
Refactor shared asset directory lookup
Copilot 2f3ec02
Document shared asset families
Copilot 38ce9f6
Avoid full artifact content scans
Copilot f881074
Clarify artifact resolution guard
Copilot 9c73550
Restore resolver core provenance
Copilot ef7fca3
Reuse artifact inventory layers
Copilot 2b8ce69
Simplify preset resolve assertion
Copilot 73832c7
Restore source-agnostic artifact provenance
Copilot bf8aeb3
artifact catalog: `id` is the source-agnostic round-trip key; `info` …
Copilot d7babbb
fix: keep layer_kind_from_lookup_id and derive_hook_id in agreement o…
Copilot 6efff92
artifact: reuse shared project resolver, rename handlers, dedupe vali…
Copilot bd483da
fix: align artifact info existence and resolver naming
Copilot e67b1cd
artifact: reuse PresetResolver.templates_dir in _project_core_asset_root
Copilot e7bdc62
fix: guard stale registry entries in artifact convention discovery
Copilot 9977faa
fix: include stack in artifact list json
Copilot d9d1ef7
docs: document artifact list stack records
Copilot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,162 @@ | ||
| # Artifacts | ||
|
|
||
| An **artifact** is any command, template, or script Spec Kit exposes in a project, regardless of which layer contributes it — built-in assets, an installed preset, an installed extension, or a project-local override in `.specify/templates/overrides/`. | ||
|
|
||
| The `specify artifact` command group is the read-only introspection surface for that inventory. `specify preset resolve <name>` answers "which file wins for this preset-managed name?"; `specify artifact` answers "what exists at all, and what is the full composition stack behind it?" — including built-in artifacts that no preset touches. | ||
|
|
||
| Both subcommands currently require `--json`. Omitting it exits with code `2` and prints a usage message on stderr; no stdout is produced. Text rendering is deliberately deferred so the JSON shapes below are the only contract, and adding a default text renderer later stays a non-breaking, additive change. | ||
|
|
||
| ## List Artifacts | ||
|
|
||
| ```bash | ||
| specify artifact list --json | ||
| ``` | ||
|
|
||
| | Option | Description | | ||
| | -------- | -------------------------------------------------------- | | ||
| | `--json` | Required. Emit the inventory as a JSON array on stdout. | | ||
|
|
||
| Prints the full inventory of every visible artifact — one row per `(kind, name)` pair, including its composition `stack` — sorted by kind (`command`, then `template`, then `script`) and then by name. | ||
|
|
||
| ```json | ||
| [ | ||
| { | ||
| "id": "command:speckit.specify", | ||
| "name": "speckit.specify", | ||
| "kind": "command", | ||
| "description": "Create or update the feature specification.", | ||
| "stack": [ | ||
| { | ||
| "id": "command:speckit.specify", | ||
| "layer": null, | ||
| "sourceId": null, | ||
| "presetId": null, | ||
| "presetName": null, | ||
| "strategy": "replace", | ||
| "active": true, | ||
| "hidden": false, | ||
| "manifestPath": null, | ||
| "lookupId": null | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "id": "script:create-new-feature", | ||
| "name": "create-new-feature", | ||
| "kind": "script", | ||
| "description": "Create a new feature branch and spec directory.", | ||
| "stack": [ | ||
| { | ||
| "id": "script:create-new-feature", | ||
| "layer": null, | ||
| "sourceId": null, | ||
| "presetId": null, | ||
| "presetName": null, | ||
| "strategy": "replace", | ||
| "active": true, | ||
| "hidden": false, | ||
| "manifestPath": null, | ||
| "lookupId": null | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| ``` | ||
|
|
||
| | Field | Description | | ||
| | ------------- | ------------------------------------------------------------------------- | | ||
| | `id` | `{kind}:{name}` — the shorthand `artifact info` accepts as its argument | | ||
| | `name` | Logical artifact name (commands use the `speckit.<stem>` namespace) | | ||
| | `kind` | One of `command`, `template`, `script` | | ||
| | `description` | Description from the highest-precedence layer that declares one, else `""` | | ||
| | `stack` | Composition stack for this artifact, using the same row shape as `artifact info` | | ||
|
|
||
| Built-in artifacts always appear, even when nothing overrides them. Descriptions come from the highest-priority layer that has one — a preset or project override that hides a built-in command reports its own description, not the hidden built-in text. Skills (`.github/skills/**/SKILL.md`) are excluded: they are integration-specific output, not a shipped asset family. | ||
|
|
||
| ## Artifact Info | ||
|
|
||
| ```bash | ||
| specify artifact info <name> --json | ||
| ``` | ||
|
|
||
| | Option | Description | | ||
| | ---------------- | ------------------------------------------------------------------- | | ||
| | `--json` | Required. Emit the composition stack as a JSON object on stdout. | | ||
| | `--kind <kind>` | Narrow the lookup to `command`, `template`, or `script` | | ||
|
|
||
| `<name>` accepts either a bare name (`speckit.specify`) or the `kind:name` shorthand (`command:speckit.specify`). When both the shorthand and `--kind` are supplied they must agree. | ||
|
|
||
| ```json | ||
| { | ||
| "id": "command:speckit.specify", | ||
| "name": "speckit.specify", | ||
| "kind": "command", | ||
| "description": "Create or update the feature specification.", | ||
| "stack": [ | ||
| { | ||
| "id": "command:speckit.specify", | ||
| "layer": "preset", | ||
| "sourceId": "compliance", | ||
| "presetId": "compliance", | ||
| "presetName": "Compliance Preset", | ||
| "strategy": "replace", | ||
| "active": true, | ||
| "hidden": false, | ||
| "manifestPath": ".specify/presets/compliance/preset.yml", | ||
| "lookupId": "preset:compliance:command:speckit.specify" | ||
| }, | ||
| { | ||
| "id": "command:speckit.specify", | ||
| "layer": null, | ||
| "sourceId": null, | ||
| "presetId": null, | ||
| "presetName": null, | ||
| "strategy": "replace", | ||
| "active": false, | ||
| "hidden": true, | ||
| "manifestPath": null, | ||
| "lookupId": null | ||
| } | ||
| ] | ||
| } | ||
| ``` | ||
|
|
||
| The top-level `id`, `name`, `kind`, `description`, and `stack` fields match the corresponding row on `artifact list --json`. | ||
|
|
||
| ### Stack semantics | ||
|
|
||
| `stack` is ordered by resolution precedence: index `0` is the layer that wins. Each row describes one contributing layer: | ||
|
|
||
| | Field | Description | | ||
| | -------------- | -------------------------------------------------------------------------------- | | ||
| | `id` | `{kind}:{name}` — the source-agnostic round-trip key, identical on every row of the same artifact's stack | | ||
| | `layer` | `project`, `preset`, or `extension`; `null` for built-in layers | | ||
| | `sourceId` | Source component of `lookupId`, or `null` when the layer has no provenance | | ||
| | `presetId` | Preset pack directory id; `null` on built-in, `project`, and `extension` rows | | ||
| | `presetName` | Preset display name when its manifest declares one, else the pack id; `null` when `presetId` is `null` | | ||
| | `strategy` | `replace`, `wrap`, `prepend`, or `append` | | ||
| | `active` | `true` only for index `0` — the layer whose content is served | | ||
| | `hidden` | `true` when a lower-index `replace` layer cuts this layer out of the composition | | ||
| | `manifestPath` | Project-relative path to the declaring manifest, or `null` when none applies | | ||
| | `lookupId` | Deterministic `{layer}:{sourceId}:{kind}:{name}` identifier, or `null` for built-in layers | | ||
|
|
||
| `active` and `hidden` are independent labels, not opposites. Composing strategies (`wrap`, `prepend`, `append`) keep lower layers in the composed output, so an inactive layer is not necessarily hidden: only layers below the first `replace` layer are marked `hidden`. Built-in rows have no provenance: `layer`, `sourceId`, and `lookupId` are `null` — but `id` is always populated, even on built-in rows. `id` is the round-trip key: `specify artifact info` accepts it as input (for example, `specify artifact info command:speckit.specify --json`), and it resolves the same artifact whether the caller passes the bare name or the `id`. | ||
|
|
||
| Lookup IDs use the same grammar as [preset contribution identifiers](presets.md#contribution-identifiers), so a `lookupId` from this command joins directly to `PresetManifest.iter_contributions()` / `ExtensionManifest.iter_contributions()` for manifest-declared layers. Project-local overrides carry a synthetic `project:_:{kind}:{name}` ID that intentionally matches no manifest contribution. `lookupId` is manifest-backed layer provenance, not the round-trip key — use `id` for that. | ||
|
|
||
| ## JSON Errors | ||
|
|
||
| On failure, nothing is written to stdout. A single-key JSON envelope is written to stderr and the process exits with code `1`: | ||
|
|
||
| ```json | ||
| { "error": "unknown artifact command:nope" } | ||
| ``` | ||
|
|
||
| | Message | Cause | | ||
| | --------------------------------------------------- | ---------------------------------------------------------------- | | ||
| | `not a Spec Kit project: no .specify/ directory found` | Run outside an initialized project | | ||
| | `unknown artifact <name>` | No artifact matches the requested name (and kind, when given) | | ||
| | `ambiguous artifact <name>: matches kinds [...]` | The bare name matches more than one kind — re-run with `--kind` | | ||
| | `artifact resolution failed` | The preset/extension registries could not be read, or artifact content could not be composed | | ||
|
|
||
| Exit code `2` is reserved for usage errors — a missing `--json` flag or an invalid `--kind` value — and emits a plain-text message on stderr rather than a JSON envelope. |
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
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
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
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
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
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.