php-agentic/Models/PlanTemplateVersion.php
Clotho 0e7b617551
Some checks failed
CI / PHP 8.3 (pull_request) Failing after 2s
CI / PHP 8.4 (pull_request) Failing after 1s
feat: add template version management (#35)
Snapshots YAML template content in a new `plan_template_versions` table
whenever a plan is created from a template. Plans reference their version
via `template_version_id` so existing plans are unaffected by future
template file edits.

Key changes:
- Migration 0006: create `plan_template_versions` table (slug, version,
  name, content JSON, content_hash SHA-256); add nullable FK
  `template_version_id` to `agent_plans`
- Model `PlanTemplateVersion`: `findOrCreateFromTemplate()` deduplicates
  identical content by hash; `historyFor()` returns versions newest-first
- `AgentPlan`: add `template_version_id` fillable and `templateVersion()`
  relationship
- `PlanTemplateService::createPlan()`: snapshot raw template before
  variable substitution; store version id and version number in metadata;
  add `getVersionHistory()` and `getVersion()` public methods
- Tests: `TemplateVersionManagementTest` covering model behaviour, plan
  creation snapshotting, deduplication, history ordering, and service
  methods

Closes #35

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 13:25:17 +00:00

92 lines
2.7 KiB
PHP

<?php
declare(strict_types=1);
namespace Core\Mod\Agentic\Models;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* Plan Template Version - immutable snapshot of a YAML template's content.
*
* When a plan is created from a template, the template content is snapshotted
* here so future edits to the YAML file do not affect existing plans.
*
* Identical content is deduplicated via content_hash so no duplicate rows
* accumulate when the same (unchanged) template is used repeatedly.
*
* @property int $id
* @property string $slug Template file slug (filename without extension)
* @property int $version Sequential version number per slug
* @property string $name Template name at snapshot time
* @property array $content Full template content as JSON
* @property string $content_hash SHA-256 of json_encode($content)
* @property \Carbon\Carbon|null $created_at
* @property \Carbon\Carbon|null $updated_at
*/
class PlanTemplateVersion extends Model
{
protected $fillable = [
'slug',
'version',
'name',
'content',
'content_hash',
];
protected $casts = [
'content' => 'array',
'version' => 'integer',
];
/**
* Plans that were created from this template version.
*/
public function plans(): HasMany
{
return $this->hasMany(AgentPlan::class, 'template_version_id');
}
/**
* Find an existing version by content hash, or create a new one.
*
* Deduplicates identical template content so we don't store redundant rows
* when the same (unchanged) template is used multiple times.
*/
public static function findOrCreateFromTemplate(string $slug, array $content): self
{
$hash = hash('sha256', json_encode($content, JSON_UNESCAPED_UNICODE));
$existing = static::where('slug', $slug)
->where('content_hash', $hash)
->first();
if ($existing) {
return $existing;
}
$nextVersion = (static::where('slug', $slug)->max('version') ?? 0) + 1;
return static::create([
'slug' => $slug,
'version' => $nextVersion,
'name' => $content['name'] ?? $slug,
'content' => $content,
'content_hash' => $hash,
]);
}
/**
* Get all recorded versions for a template slug, newest first.
*
* @return Collection<int, static>
*/
public static function historyFor(string $slug): Collection
{
return static::where('slug', $slug)
->orderByDesc('version')
->get();
}
}