Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: quality

on:
push:
branches:
- main
- "*.x"
pull_request:

jobs:
pint:
name: Code style
runs-on: ubuntu-24.04

steps:
- name: Checkout code
uses: actions/checkout@v5

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: "8.4"
tools: composer:v2
coverage: none

- name: Install dependencies
run: composer install --prefer-dist --no-interaction --no-progress

- name: Check formatting
run: composer test:lint

changelog:
name: Changelog parity
runs-on: ubuntu-24.04

steps:
- name: Checkout code
uses: actions/checkout@v5

# CHANGELOG.md and the documentation site's changelog page are written
# separately and drift silently. Every released version must appear in
# both, in the same order.
- name: Compare CHANGELOG.md against the documentation changelog
run: |
python3 - <<'PY'
import re, sys
from pathlib import Path

repo = Path("CHANGELOG.md").read_text()
docs = Path("docs/changelog.mdx").read_text()

# "## [0.4.5] - 2026-08-03" -> 0.4.5, ignoring [Unreleased].
in_repo = re.findall(r"^## \[(\d[^\]]*)\]", repo, re.M)

# '<Update label="v0.4.5" ...>' -> 0.4.5
in_docs = [v.lstrip("v") for v in re.findall(r'<Update\s+label="([^"]+)"', docs)]

problems = []

for missing in [v for v in in_repo if v not in in_docs]:
problems.append(f"{missing} is in CHANGELOG.md but has no <Update> in docs/changelog.mdx")

for missing in [v for v in in_docs if v not in in_repo]:
problems.append(f"{missing} has an <Update> in docs/changelog.mdx but no CHANGELOG.md entry")

if not problems and in_repo != in_docs:
problems.append(
"both files list the same versions in a different order\n"
f" CHANGELOG.md : {' '.join(in_repo)}\n"
f" docs/changelog.mdx : {' '.join(in_docs)}"
)

if problems:
print("Changelog parity check failed:\n")
for p in problems:
print(f" - {p}")
print("\nUpdate both files so each release appears in each, newest first.")
sys.exit(1)

print(f"OK - {len(in_repo)} releases match across both changelogs.")
print(f" {' '.join(in_repo)}")
PY
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,4 @@ jobs:
--with="orchestra/testbench=${{ matrix.testbench }}"

- name: Execute tests
run: vendor/bin/pest
run: composer test
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Removed

## [0.4.5] - 2026-08-03

### Added

- Added a `quality` workflow running the Pint formatting check on every push and pull request, and asserting that every release in `CHANGELOG.md` has a matching entry in the documentation site's changelog. Formatting was previously never checked in CI, and the two changelogs could drift silently.

### Changed

- `Deck::activate()` now accepts `string|int` versions, so `'v2'`, `'2'`, and `2` are all valid. Only `Deck::get()` was widened in `0.4.2`, despite the changelog describing both.
- The `tests` workflow now runs `composer test` rather than calling `vendor/bin/pest` directly, so CI and the documented contributor command cannot diverge.

### Fixed

- Fixed an unparseable version producing a message with an empty version number, such as `Version for prompt [order-summary] does not exist.` Both `Deck::get()` and `Deck::activate()` now throw `InvalidVersionException` naming the offending value.
- Fixed the README prompt structure diagram, which showed a `user.md` that `make:prompt` does not create without `--user`, a second version that a single run does not create, and omitted the version-level `metadata.json` added in `0.4.4`.
- Fixed the prompt structure diagrams on the introduction, configuration, prompts, and make:prompt documentation pages, which all omitted the version-level `metadata.json`.
- Fixed the README describing new versions as activating automatically. Creating a version has not changed the active version since `0.4.4`.
- Fixed the documented `Deck::get()` signature, which described `?int` rather than `string|int|null`.

### Removed

## [0.4.4] - 2026-08-03

### Fixed
Expand Down
70 changes: 61 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,28 @@ php artisan make:prompt order-summary

This creates the following structure

```txt
resources/prompts/order-summary/
├── metadata.json # Prompt-level: name, description, roles, active version
└── v1/
├── metadata.json # This version's own metadata
└── system.md
```

Pass `--user` to add a user prompt, or `--role=` for any other role

```bash
php artisan make:prompt order-summary --user --role=assistant
```

```txt
resources/prompts/order-summary/
├── metadata.json
├── v1/
│ ├── system.md
│ └── user.md
└── v2/
└── v1/
├── metadata.json
├── system.md
└── user.md
├── user.md
└── assistant.md
```

Edit `resources/prompts/order-summary/v1/system.md` with your prompt content. Use `{{ $variable }}` syntax for dynamic values:
Expand Down Expand Up @@ -87,14 +100,46 @@ $messages = $prompt->toMessages(['tone' => 'friendly', 'order' => $orderDetails]

### Versioning

Create a new version of an existing prompt
Run the command again on an existing prompt and Deck offers to create the next version or overwrite the current one

```bash
php artisan make:prompt order-summary
# Automatically creates v2, v3, etc.

# Prompt [order-summary] already exists at version 1.
# What would you like to do?
# [version ] Create a new version (v2)
# [overwrite] Overwrite version 1
```

Activate a specific version
Creating a version never changes which one your application serves, so you can draft freely in production

```txt
resources/prompts/order-summary/
├── metadata.json # "active_version": 1
├── v1/ # live: what Deck::get('order-summary') returns
│ ├── metadata.json
│ └── system.md
└── v2/ # drafted, not serving traffic yet
├── metadata.json
└── system.md
```

Check which version is live at any time

```bash
php artisan prompt:list --all
```

```txt
+---------------+----------------+--------+-------------+
| Prompt | Active Version | Active | Description |
+---------------+----------------+--------+-------------+
| order-summary | v1 | ✅ | |
| order-summary | v2 | | |
+---------------+----------------+--------+-------------+
```

Promote the new version when you are ready

```bash
php artisan prompt:activate order-summary v2
Expand All @@ -104,10 +149,17 @@ php artisan prompt:activate order-summary v2
php artisan prompt:activate order-summary 2
```

Or load a specific version programmatically
The `active_version` key in the prompt's root `metadata.json` flips to `2`, and every `Deck::get('order-summary')` call starts returning v2. Roll back by activating v1 again.

Both formats work programmatically too

```php
// Load a specific version.
$prompt = Deck::get('order-summary', 'v2');

// Promote a version.
Deck::activate('order-summary', 'v2');
Deck::activate('order-summary', 2);
```

### Laravel AI SDK Integration
Expand Down
9 changes: 6 additions & 3 deletions docs/advanced/api-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,18 @@ $versions = $manager->versions('order-summary');
// ]
```

#### `activate(string $name, int $version): bool`
#### `activate(string $name, string|int $version): bool`

Activate a specific version of a prompt. Returns `true` on success.

Accepts the same version formats as `get()`, so `2`, `'2'`, and `'v2'` are equivalent. A version that cannot be parsed throws `InvalidVersionException`.

- **With tracking enabled:** Updates the `prompt_versions` database table.
- **Without tracking:** Writes to `metadata.json` in the prompt directory.

```php
$manager->activate('order-summary', 2);
$manager->activate('order-summary', 'v2');
```

#### `track(string $promptName, int $version, array $data): void`
Expand Down Expand Up @@ -222,10 +225,10 @@ Static proxy to the `PromptManager` singleton.

| Method | Returns | Description |
| ------------------------------------------------------------ | ---------------- | ------------------------------------------- |
| `Deck::get(string $name, ?int $version = null)` | `PromptTemplate` | Load a prompt by name and optional version. |
| `Deck::get(string $name, string\|int\|null $version = null)` | `PromptTemplate` | Load a prompt by name and optional version. |
| `Deck::active(string $name)` | `PromptTemplate` | Load the active version of a prompt. |
| `Deck::versions(string $name)` | `array` | List all versions for a prompt. |
| `Deck::activate(string $name, int $version)` | `bool` | Activate a specific version. |
| `Deck::activate(string $name, string\|int $version)` | `bool` | Activate a specific version. |
| `Deck::track(string $name, int $version, array $data)` | `void` | Record a prompt execution. |

---
Expand Down
25 changes: 25 additions & 0 deletions docs/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,31 @@ rss: true

Deck follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Subscribe to the [RSS feed](https://deck.promptphp.com/changelog/rss.xml) to be notified of new releases, or browse the [full history on GitHub](https://github.com/promptphp/deck/blob/0.x/CHANGELOG.md).

<Update label="v0.4.5" description="3 August 2026" tags={["Changed", "Fixed", "Added"]}>

**Added**

- A `quality` CI workflow now checks formatting with Pint on every push and pull request, and asserts that every release in the repository `CHANGELOG.md` has a matching entry on this page. Formatting was never checked in CI before, and the two changelogs could drift apart unnoticed.
- The test workflow now runs `composer test` rather than calling Pest directly, so CI and the documented contributor command cannot diverge.

**Changed**

- **`Deck::activate()` now accepts string versions.** `'v2'`, `'2'`, and `2` are all valid, matching what `Deck::get()` has accepted since `v0.4.2`. Only `get()` was widened at the time, so `Deck::activate('order-summary', 'v2')` raised a `TypeError` despite the release notes describing both. Passing an integer continues to work unchanged.

```php
Deck::activate('order-summary', 'v2');
Deck::activate('order-summary', 2);
```

**Fixed**

- **Unparseable versions now name the offending value.** Both `Deck::get()` and `Deck::activate()` threw a message with the version missing entirely — `Version for prompt [order-summary] does not exist.` They now throw `InvalidVersionException` reading `Invalid version [banana] for prompt [order-summary]. Use a positive number like [1] or [v1].`
- Corrected the prompt structure diagrams throughout the documentation and README. They omitted the version-level `metadata.json` introduced in `v0.4.4`, and the README showed a `user.md` that `make:prompt` does not create without `--user` alongside a second version that a single run does not create.
- Corrected the README describing new versions as becoming active automatically. Creating a version has not changed the active version since `v0.4.4`.
- Corrected the documented `Deck::get()` signature, which read `?int` rather than `string|int|null`.

</Update>

<Update label="v0.4.4" description="3 August 2026" tags={["Fixed", "Added"]}>

**Fixed**
Expand Down
9 changes: 5 additions & 4 deletions docs/core/make-prompt.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@ This generates the following structure inside your configured prompts directory
```
resources/prompts/
└── order-summary/
├── v1/
│ └── system.md
└── metadata.json
├── metadata.json # Prompt-level: name, description, roles, active version
└── v1/
├── metadata.json # This version's own metadata
└── system.md
```

A **system prompt** file is always created. A `metadata.json` file is placed at the prompt root to record the prompt's name, description, roles, and creation timestamp.
A **system prompt** file is always created. Two `metadata.json` files are written: one at the prompt root recording the prompt's name, description, roles, and creation timestamp, and one inside the version directory recording that version alone.

### Interactive mode

Expand Down
19 changes: 13 additions & 6 deletions docs/core/prompts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -290,16 +290,20 @@ Prompts are versioned using directory-based versioning. Each version lives in it
```
resources/prompts/
└── order-summary/
├── v1/
├── metadata.json # "active_version": 1
├── v1/ # live
│ ├── metadata.json
│ ├── system.md
│ └── user.md
── v2/
├── system.md
├── user.md
── assistant.md
└── metadata.json
── v2/ # drafted, not serving traffic yet
├── metadata.json
├── system.md
── user.md
└── assistant.md
```

Only the active version is served. Creating a new version leaves `active_version` untouched, so you can draft in production and promote with [`prompt:activate`](/core/commands#promptactivate) when ready.

Each version directory can contain:

- Any number of role files (e.g. `system.md`, `user.md`, `assistant.md`)
Expand Down Expand Up @@ -334,8 +338,11 @@ Set a specific version as the active version:

```php
Deck::activate('order-summary', 2);
Deck::activate('order-summary', 'v2');
```

The version accepts the same formats as `Deck::get()` — `2`, `'2'`, and `'v2'` are equivalent. Anything that cannot be parsed throws `InvalidVersionException`.

**When database tracking is enabled**, this updates the `prompt_versions` table — setting `is_active = false` on all versions of that prompt, then `is_active = true` on the specified version.

**When tracking is disabled**, it writes the `active_version` key to the prompt's root `metadata.json` file.
Expand Down
12 changes: 7 additions & 5 deletions docs/getting-started/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,14 @@ Currently, only the `directory` strategy is supported. Each version is stored in

```
resources/prompts/order-summary/
├── v1/
├── metadata.json # "active_version": 1
├── v1/ # live
│ ├── metadata.json
│ └── system.md
── v2/
├── system.md
── user.md
└── metadata.json
── v2/ # drafted, not serving traffic yet
├── metadata.json
── system.md
└── user.md
```

## Cache
Expand Down
7 changes: 4 additions & 3 deletions docs/getting-started/introduction.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ This creates the following structure:
```
resources/prompts/
└── order-summary/
├── v1/
│ └── system.md
└── metadata.json
├── metadata.json # Prompt-level: name, description, roles, active version
└── v1/
├── metadata.json # This version's own metadata
└── system.md
```

Edit `resources/prompts/order-summary/v1/system.md` with your prompt content. Use `{{ $variable }}` syntax for dynamic values:
Expand Down
13 changes: 13 additions & 0 deletions src/Exceptions/InvalidVersionException.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,17 @@ public static function noVersions(string $name): self
{
return new self("No versions found for prompt [{$name}].");
}

/**
* Create an exception for a version that could not be parsed.
*
* @param string $name The name of the prompt.
* @param string|int $version The version input that could not be parsed.
*/
public static function unparseable(string $name, string|int $version): self
{
return new self(
"Invalid version [{$version}] for prompt [{$name}]. Use a positive number like [1] or [v1]."
);
}
}
Loading