# Skill Version Management Design

Date: 2026-06-30

## Background

The platform currently stores each Skill as one mutable row in `skills`. The UI displays `v1.0.0`, but that value is generated on the frontend and is not persisted. Publishing only updates the current Skill status, and runtime calls read the current `skills.package_key`.

This design adds version management for one primary goal: every publish is traceable and any published version can be restored.

## Goals

- Persist a version snapshot on every publish.
- Keep one current online version per Skill.
- Allow users to view version history and download any historical package.
- Allow owners and super admins to roll back to a previous published version.
- Preserve auditability: rollback creates a new version record instead of rewriting history.

## Non-Goals

- Multiple live versions of the same Skill are not supported in this phase.
- API callers will not choose a version ID during invocation.
- Existing API keys remain bound to the Skill, not to individual versions.

## Recommended Approach

Use a current-state table plus a historical snapshot table.

- `skills` remains the source of the current Skill state.
- A new `skill_versions` table stores immutable publish snapshots.
- `skills.version` stores the current display version.
- `skills.current_version_id` points to the current row in `skill_versions`.

This keeps the runtime path simple while adding real traceability.

## Data Model

Add migration `0014_skill_versions.sql`.

### `skills` Additions

- `version TEXT NOT NULL DEFAULT 'v0.0.0'`
- `current_version_id TEXT`

### `skill_versions`

- `id TEXT PRIMARY KEY`
- `skill_id TEXT NOT NULL`
- `version TEXT NOT NULL`
- `status TEXT NOT NULL DEFAULT 'published'`
- `name TEXT NOT NULL`
- `description TEXT`
- `node_id TEXT NOT NULL`
- `node_path TEXT NOT NULL`
- `source TEXT NOT NULL`
- `visibility TEXT NOT NULL`
- `creator_name TEXT`
- `creator_phone TEXT`
- `creator_email TEXT`
- `package_key TEXT`
- `package_size INTEGER`
- `package_name TEXT`
- `changelog TEXT`
- `published_by_phone TEXT`
- `published_by_email TEXT`
- `source_version_id TEXT`
- `snapshot_json TEXT NOT NULL`
- `created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))`

Indexes:

- `(skill_id, created_at DESC)`
- unique `(skill_id, version)`
- `(skill_id, status)`

The `snapshot_json` field stores the full publish snapshot. The named columns keep common list and rollback operations simple.

## Version Numbering

Use semantic-looking platform versions:

- First publish becomes `v1.0.0`.
- Each later publish increments the minor number: `v1.1.0`, `v1.2.0`.
- Rollback also creates a new version using the next minor number.

Rollback does not restore the old version number as current. For example, rolling back from `v1.3.0` to `v1.1.0` creates `v1.4.0` with changelog `回滚自 v1.1.0`.

## Publish Flow

Current frontend calls already update a Skill to `status: 'published'`. The backend `PUT /api/skills/:id` should detect this transition and create a version snapshot.

When status is set to `published`:

1. Validate ownership or super admin permission.
2. Apply allowed field updates to `skills`.
3. Compute next version from the latest `skill_versions.version`, falling back to `v1.0.0`.
4. Insert a `skill_versions` row from the fresh `skills` row.
5. Update `skills.version` and `skills.current_version_id`.
6. Record audit action `skill.publish`.

If a Skill is already published and receives another `status: 'published'` update, treat it as a new publish only when publish-relevant fields changed or a `force_new_version` flag is provided. This prevents accidental duplicate versions from repeated saves.

Publish-relevant fields:

- `name`
- `description`
- `node_id`
- `node_path`
- `visibility`
- `package_key`
- `package_size`
- `package_name`

## Rollback Flow

Add endpoint:

`POST /api/skills/:id/versions/:versionId/rollback`

Behavior:

1. Validate ownership or super admin permission.
2. Load target version and ensure it belongs to the Skill.
3. Copy the target snapshot fields back to `skills`.
4. Keep `skills.status = 'published'`.
5. Create a new `skill_versions` row with the next version number.
6. Set `source_version_id` to the target version ID.
7. Set changelog to user-provided text or `回滚自 <target version>`.
8. Record audit action `skill.rollback`.

History remains immutable. The rollback result is a new current version.

## API Changes

### List and Detail

Include `version` and `current_version_id` in:

- `GET /api/skills`
- `GET /api/skills/:id`

### Version History

Add:

- `GET /api/skills/:id/versions`

Returns versions sorted newest first. Visibility follows the same rule as Skill detail.

### Publish Metadata

Allow optional fields on publish:

- `changelog`
- `force_new_version`

### Rollback

Add:

- `POST /api/skills/:id/versions/:versionId/rollback`

Body:

```json
{
  "changelog": "回滚原因"
}
```

## Frontend Changes

### Data Mapping

Remove hard-coded `v1.0.0` values in `src/services/api.js`. Use backend `s.version`, falling back to `v0.0.0` for old rows.

### Skill Edit Page

Add a compact "版本记录" section below metadata:

- Current version badge.
- Version list with version number, publish time, publisher, package name, changelog.
- Download button for each package.
- Rollback button for non-current versions when the user can edit.

Rollback should ask for confirmation before calling the endpoint.

### Skill Detail Modal

Show the real version near status and update date. Historical rollback can stay in the edit page to keep the public detail modal focused.

## Runtime Behavior

Runtime calls continue to use the current `skills.package_key`. After rollback, the current Skill row points to the restored package, so existing API keys automatically call the restored version.

`skill_invocations` can keep its current schema for this phase. A later migration may add `skill_version` and `skill_version_id` to invocation logs if runtime-level version analytics become important.

## Permissions

Use existing rules:

- Owners can publish and roll back their own Skills.
- Super admins can publish and roll back any Skill.
- Other users can view only public Skills or private Skills they own.

## Error Handling

- Missing Skill: `404 not_found`
- Missing version: `404 version_not_found`
- Version belongs to another Skill: `404 version_not_found`
- Unauthorized edit or rollback: `403 forbidden`
- Rollback without package is allowed, but the UI should show package as empty.

## Testing

Backend tests:

- Publishing a draft creates `v1.0.0`.
- Publishing changed content creates the next version.
- Re-saving an already published Skill without publish-relevant changes does not create duplicate versions.
- Version history is returned newest first.
- Rollback restores package and description to the Skill row.
- Rollback creates a new version with `source_version_id`.
- Non-owner rollback is forbidden.

Frontend tests or focused verification:

- Skill cards and detail views show backend versions.
- Edit page renders version history.
- Rollback refreshes the Skill detail and current version.

## Rollout

1. Add database migration.
2. Add backend helper functions for version computation and snapshot creation.
3. Extend existing Skill routes.
4. Update frontend API mapping and Skill views.
5. Add tests.
6. Run existing build and backend route tests.

