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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ on:
push:
branches: [develop]

permissions:
contents: read

jobs:
ci:
uses: pilgrimagesoftware/github-actions/.github/workflows/rust-ci.yaml@master
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/prepare-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ name: Prepare Release
on:
workflow_dispatch:

permissions:
contents: write
pull-requests: write

jobs:
prepare:
uses: pilgrimagesoftware/github-actions/.github/workflows/rust-prepare-release.yaml@master
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ on:
required: true
type: string

permissions:
contents: write
id-token: write

jobs:
release:
uses: pilgrimagesoftware/github-actions/.github/workflows/rust-release.yaml@master
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/tag-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ on:
branches:
- master

permissions:
contents: write

jobs:
tag-release:
uses: pilgrimagesoftware/github-actions/.github/workflows/rust-tag-release.yaml@master
Expand Down
27 changes: 24 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,39 @@

## [0.1.1] - 2026-07-20
## [0.2.0] - 2026-07-21


### Added

- Add name and localized [names] display-name fields


### Documentation

- Drop spurious "Fixed: Conflicts" changelog entry

- Document squash-merge subject requirement in RELEASING.md

- Fix wrong export! form and stale 0.2 version references


### Fixed

- Conflicts

- Add explicit permissions to CI/release caller workflows



## [Unreleased]

## [0.1.0] - 2026-07-20
### Added

- Add a required `name` field to `Manifest` for a plugin's human-readable display name,
distinct from `id` (hosts must not derive a display name from `id`)
- Add an optional `[names]` table for locale-keyed display names, and
`Manifest::localized_name` to look one up with fallback to `name`

## [Unreleased]
## [0.1.1] - 2026-07-20

### Added

Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "fulltime-plugin-api"
version = "0.1.1"
version = "0.2.0"
edition = "2021"
rust-version = "1.85"
description = "Canonical league-data schema, data-provider WIT interface, and plugin manifest format shared by the FullTime plugin host and data-provider plugins."
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Neither the host nor any plugin owns this contract - it is versioned and publish
Rust bindings are generated from this file via `wit-bindgen`, not hand-written, so the WIT source is the single source of truth.
- **Plugin manifest format** (`Manifest`): the static TOML file every plugin ships declaring its ID, release version, targeted schema/interface versions, and required network hosts.
This crate validates structure and field format only - network reachability and capability enforcement belong to the host runtime (`Apps/rust`).
- **`host` interface and `Guest`/`export!` bindings**: `world plugin` imports `host.fetch` - a plugin has no direct network access and must call this crate's `host_fetch` wrapper for every upstream request. This crate also re-exports the generated `Guest` trait and `export!` macro so a downstream plugin implements and exports the world using this crate's own canonical types, rather than regenerating an incompatible copy from a vendored WIT file.

## Versioning

Expand All @@ -29,7 +30,7 @@ See [`Version::accepts`].

```toml
[dependencies]
fulltime-plugin-api = "0.2"
fulltime-plugin-api = "0.1"
```

```rust
Expand Down
4 changes: 4 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
Releases are driven by [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) on `develop` and [`git-cliff`](https://git-cliff.org), via three workflows in `.github/workflows/` (thin wrappers around the `rust-*` reusable workflows in [`pilgrimagesoftware/github-actions`](https://github.com/pilgrimagesoftware/github-actions)).
There's no manual version bumping or changelog editing.

Because of this, **squash-merging a feature PR into `develop` must preserve the original commit's Conventional Commits type and any `!`/`BREAKING CHANGE:` marker.**
GitHub's default squash message is the PR title, which usually drops both — `git-cliff` then can't classify the squashed commit, silently produces an empty/no-op changelog section, and under-bumps the version.
Set the squash subject explicitly, e.g. `gh pr merge <n> --squash --subject "feat!: <description>"`, or merge with a merge commit instead when the PR is a single already-well-formed commit.

## 1. Prepare the release

Trigger **Prepare Release** manually (Actions tab → Prepare Release → Run workflow). It:
Expand Down
11 changes: 7 additions & 4 deletions docs/plugin-authoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,10 @@ types from a vendored WIT file:
this crate's own `Team`/`Fixture`/`Standings`/`Competition`/`ProviderError` types
directly.
- [`export!`] — the macro that exports your `Guest` implementation as the component's
`data-provider` interface.
`data-provider` interface. Called from a downstream crate, it needs the `with_types_in`
form — the single-arg form only resolves inside this crate itself, since `export!` is
`wit-bindgen`-generated and expects to find its supporting types in the crate that
declares them.

```rust,ignore
struct MyPlugin;
Expand All @@ -133,19 +136,19 @@ impl fulltime_plugin_api::Guest for MyPlugin {
// fetch_fixtures, fetch_results, fetch_standings, fetch_metadata ...
}

fulltime_plugin_api::export!(MyPlugin);
fulltime_plugin_api::export!(MyPlugin with_types_in fulltime_plugin_api);
```

## Getting started

1. Add this crate as a dependency:
```toml
[dependencies]
fulltime-plugin-api = "0.2"
fulltime-plugin-api = "0.1"
```
2. Implement [`Guest`] against your upstream data source, mapping its response shape into
the canonical schema types, and calling [`host_fetch`] for every upstream request.
3. Call [`export!`] with your implementation.
3. Call [`export!`] with the `with_types_in` form against your implementation.
4. Write your `manifest.toml` declaring the network hosts you call and `interface_version
= "2.0"`.
5. Build to a WASM component target and load it against the host runtime in `Apps/rust`.
Expand Down
2 changes: 2 additions & 0 deletions openspec/changes/add-plugin-build-metadata/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-07-21
83 changes: 83 additions & 0 deletions openspec/changes/add-plugin-build-metadata/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
## Context

The plugin manifest (`src/manifest.rs`) currently carries `id`, `version`, `schema_version`,
`interface_version`, and `network_hosts` — enough for the host to load and version-check a
plugin, but nothing to show a human which developer/publisher built it or when. `Apps/rust`'s
Plugins management screen (`openspec/changes/plugin-host-runtime`, already implemented there)
lists `id` and `version` only, for exactly this reason.

Two prior manifest fields established a validation precedent worth following here:
`schema_version`/`interface_version` are parsed into a typed [`Version`] because the host
actively compares them for compatibility. `network_hosts` entries get only a "must not be empty"
check because they're used as-is (string equality against a request's host). This change's two
new fields are purely informational — nothing compares or parses them — so they follow the
`network_hosts` precedent, not the `Version` one.

## Goals / Non-Goals

**Goals:**
- Let a plugin manifest optionally declare a developer/publisher display name and a build
timestamp.
- Keep every existing manifest (in particular `Plugins/Bundesliga`'s) parsing unchanged with no
edits required — an additive, minor-version change per this crate's own versioning policy.

**Non-Goals:**
- Validating `build_date` as a well-formed timestamp. This crate never validates `Fixture.kickoff`
(also documented as RFC 3339) either; adding parsing here would be inconsistent and would pull
in a date/time dependency (`time` or `chrono`) this crate has never needed, bloating every
plugin's compiled `wasm32` component for a display-only field.
- Any host-side or UI-side consumption of these fields. Surfacing them in `Apps/rust`'s Plugins
screen is a separate, follow-up change in that repo.
- Making either field required. That would be a breaking, major-version change forcing every
existing plugin (starting with `Plugins/Bundesliga`) to update its manifest before it could be
loaded by a host built against the new version.

## Decisions

**Both fields are `Option<String>`, not a new typed wrapper.** `developer` is a free-form display
string (no format to validate beyond non-empty). `build_date` is documented as RFC 3339 but stored
and returned as the raw string, exactly like `Fixture.kickoff` — this crate parses neither.
Alternative considered: a `Version`-style typed date wrapper with parse validation, rejected per
the Non-Goals above (inconsistent with `kickoff`, needless dependency, no consumer that needs a
parsed value yet).

**Both fields are optional, not required.** Alternative considered: required fields, rejected
because it forces a major version bump and breaks every existing manifest, for two fields whose
absence is a completely reasonable state (a plugin author who hasn't set up a build-date stamping
step yet, or doesn't want to disclose a developer name).

**Validation mirrors `network_hosts`, not `schema_version`.** When present, each field must be a
non-empty string after trimming (same rule `network_hosts` entries already use) — not a schema
compatibility concern, so no `ManifestField` variant needs special version-parsing logic, just the
same "field is present but empty" rejection path `network_hosts` already has.

## Risks / Trade-offs

- [A future need to actually parse `build_date` (e.g. to sort plugins by recency) would require
revisiting the no-validation decision] → Acceptable now: no consumer needs a parsed value yet,
and adding validation later is itself another additive, non-breaking change (tightening an
`Option<String>` to reject previously-accepted malformed strings would be the only breaking
edge case, and is deferred to if/when it's actually needed).
- [`developer` has no format constraint at all, so two plugins could declare visually-identical or
confusingly-similar developer names] → Out of scope: this crate validates manifest structure,
not developer identity or trust — matching its existing stated non-goal for `network_hosts`
("this crate validates manifest format only").

## Migration Plan

1. Add both fields to `RawManifest` and `Manifest`, both `Option<String>`, with the non-empty
check applied only when present.
2. Bump `Cargo.toml`'s version per this being an additive/minor change (handled by the normal
`git-cliff`-driven release process in `RELEASING.md`, not a manual step here).
3. No manifest anywhere needs to change for this to ship — `Plugins/Bundesliga`'s current
`manifest.toml` keeps parsing exactly as it does today, with both new fields resolving to
`None`.

Rollback: revert the two-field addition; no data migration exists since nothing is persisted by
this crate itself.

## Open Questions

- Should `Apps/rust`'s Plugins screen surface these fields once available? Deferred to a
follow-up change in that repo, coordinated after this one ships and a new `fulltime-plugin-api`
version is cut.
45 changes: 45 additions & 0 deletions openspec/changes/add-plugin-build-metadata/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
## Why

The plugin manifest currently has no field for who built a plugin or when. `fulltime-core`'s
Plugins management screen (`openspec/changes/plugin-host-runtime` in `Apps/rust`) lists each
plugin's `id` and `version` only, because that's all the manifest carries — there's nowhere to
show a developer/publisher name or a build timestamp to help a user tell plugins apart or judge
how current one is.

## What Changes

- Add two optional manifest fields: `developer` (a display name/identifier for the plugin's
author or publisher) and `build_date` (an RFC 3339 timestamp for when the plugin was built).
Optional, not required, so existing manifests (e.g. `Plugins/Bundesliga`'s) keep parsing
without changes — an additive, minor-version manifest schema change under this crate's own
versioning policy (see `RELEASING.md`/`src/version.rs`'s doc comments).
- `Manifest::parse` accepts and exposes both fields when present, and treats their absence as
`None` rather than a parse error. Neither field affects host/plugin compatibility checks — both
are display-only metadata, unlike `schema_version`/`interface_version`.
- `build_date` is documented as RFC 3339 (matching the existing `Fixture.kickoff` convention in
`wit/data-provider.wit`) but is not parsed/validated by this crate — same treatment as
`kickoff`, which this crate also never validates. A non-empty check only, matching
`network_hosts` entries.

## Capabilities

### New Capabilities

(none)

### Modified Capabilities

- `plugin-manifest-format`: the manifest schema gains two optional fields, `developer` and
`build_date`, each exposed on the parsed `Manifest` and validated at parse time when present.

## Impact

- **`src/manifest.rs`**: `Manifest` struct gains `developer: Option<String>` and
`build_date: Option<String>` (or a parsed timestamp type — see `design.md`), `RawManifest`
gains the corresponding optional fields, and `Manifest::parse` validates `build_date`'s format
when present.
- **Downstream plugins** (`Plugins/Bundesliga`, future plugins): unaffected unless they choose to
add the new fields to their own `manifest.toml`.
- **`Apps/rust`'s plugin management UI** (`openspec/changes/plugin-host-runtime`, a separate,
already-implemented change in that repo): a follow-up change there would surface these fields
in the Plugins screen once this manifest change ships — out of scope here.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
## ADDED Requirements

### Requirement: Plugin Build Metadata
The manifest schema SHALL support two optional display-only fields: `developer` (a
display name/identifier for the plugin's author or publisher) and `build_date` (a
timestamp, conventionally RFC 3339, for when the plugin was built). Neither field SHALL be
required, and neither SHALL affect schema/interface compatibility checks.

#### Scenario: Manifest omits both fields
- **WHEN** a manifest has no `developer` or `build_date` field
- **THEN** parsing succeeds and the parsed manifest exposes both as absent, not as an error

#### Scenario: Manifest declares a developer name
- **WHEN** a manifest includes a non-empty `developer` field
- **THEN** the parsed manifest exposes that value unchanged

#### Scenario: Manifest declares a build date
- **WHEN** a manifest includes a non-empty `build_date` field
- **THEN** the parsed manifest exposes that value unchanged, without being parsed or validated
as a timestamp

#### Scenario: Empty developer or build_date field is rejected
- **WHEN** a manifest includes a `developer` or `build_date` field present but empty (or
whitespace-only)
- **THEN** parsing fails with a structured error identifying the invalid field, the same way an
empty `network_hosts` entry is rejected
33 changes: 33 additions & 0 deletions openspec/changes/add-plugin-build-metadata/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
## 1. Manifest Schema

- [ ] 1.1 Add `developer: Option<String>` and `build_date: Option<String>` to `Manifest` in
`src/manifest.rs`, each with a doc comment noting `build_date` is conventionally RFC 3339 but
unvalidated (matching `Fixture.kickoff`'s treatment)
- [ ] 1.2 Add the corresponding optional fields to `RawManifest`
- [ ] 1.3 Add `ManifestField::Developer` and `ManifestField::BuildDate` variants, including their
`Display` impl arm

## 2. Parsing and Validation

- [ ] 2.1 In `Manifest::parse`, thread both new fields through as `Option<String>`, defaulting to
`None` when absent
- [ ] 2.2 Reject a present-but-empty/whitespace-only `developer` or `build_date` with
`ManifestError::InvalidField`, reusing (or extracting into a shared helper alongside)
`network_hosts`'s existing empty-entry check

## 3. Tests

- [ ] 3.1 Unit test: manifest omitting both fields parses successfully with both `None`
- [ ] 3.2 Unit test: manifest declaring both fields parses successfully and exposes them
unchanged
- [ ] 3.3 Unit test: empty `developer` field is rejected with
`ManifestField::Developer`
- [ ] 3.4 Unit test: empty `build_date` field is rejected with `ManifestField::BuildDate`
- [ ] 3.5 Update the crate-level doc example in `src/lib.rs` and/or `README.md` if either shows a
full manifest, so they stay accurate (additive fields, no required change, but worth checking)

## 4. Release

- [ ] 4.1 Update `CHANGELOG.md`'s `[Unreleased]` section describing the additive manifest change
- [ ] 4.2 Confirm `RELEASING.md`'s process results in a minor version bump (additive manifest
field), not a patch or major
34 changes: 33 additions & 1 deletion openspec/specs/data-provider-plugin-api/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,35 @@ failures surface as unhandled traps.
- **THEN** the plugin returns the `schema-mapping-failure` error variant rather than
partial or malformed schema data

### Requirement: Downstream Implementation Bindings
This crate SHALL expose the generated `Guest` trait and `export!` macro for the
`data-provider` interface, so a downstream plugin can implement and export the world using
this crate's own canonical types instead of regenerating an incompatible copy from a
vendored WIT file.

#### Scenario: Plugin implements the Guest trait
- **WHEN** a plugin crate depends on this crate as an ordinary Rust library
- **THEN** it can implement this crate's re-exported `Guest` trait for `data-provider`
using this crate's own `Team`/`Fixture`/`Standings`/`Competition`/`ProviderError` types,
with no separate WIT-derived type set of its own

#### Scenario: Plugin exports its implementation
- **WHEN** a plugin has implemented the `Guest` trait
- **THEN** it calls this crate's re-exported `export!` macro to export the implementation
as the component's `data-provider` interface, without needing its own
`wit_bindgen::generate!` invocation

### Requirement: Interface Versioning
The data-provider interface SHALL carry an explicit version identifier, independent of the
schema version, so the host can detect and reject plugins built against an incompatible
interface version before invoking them.

`INTERFACE_VERSION`'s major component covers both axes of compatibility: the shape of the
`data-provider` exports a plugin implements, and the set of imports (currently, `host.fetch`)
a plugin requires from the host. A change to either axis that a plugin built against an
older major version cannot satisfy is a major bump; before `host.fetch` existed, only the
export shape was covered.

#### Scenario: Plugin built against a newer interface than the host supports
- **WHEN** the host loads a plugin declaring an interface version newer (major) than any
version the host implements
Expand All @@ -55,4 +79,12 @@ interface version before invoking them.
- **WHEN** the host loads a plugin declaring an interface minor version lower than the
host's supported version, with the same major version
- **THEN** the host loads the plugin, since the host's interface is a superset of the
functions the plugin was built against
functions the plugin was built against, and the plugin requires no imports the host
cannot supply

#### Scenario: Plugin built before the host-fetch import existed
- **WHEN** the host loads a plugin declaring `interface_version` `1.x` (built before
`host.fetch` was added to the `plugin` world)
- **THEN** the host refuses to load the plugin as a major-version mismatch against its own
`2.x` support, rather than attempting instantiation and failing at the component-linking
stage with a less informative error
Loading
Loading