diff --git a/README.md b/README.md index 02300f63..ded84edd 100644 --- a/README.md +++ b/README.md @@ -90,8 +90,8 @@ cargo agents sync Symposium reads the workspace dependency graph and evaluates each plugin's and skill's predicates against it. An extension is installed only when its predicates match. -- The `crates` field matches by crate name and version requirement (`serde`, `serde >= 1.0`, `*`). -- The `predicates` field uses function-call syntax (`crate(...)`, `shell(...)`, `path_exists(...)`, `env(...)`), combined with `not`, `any`, and `all`. +- The `depends-on` field matches by crate name and version requirement (`serde`, `serde >= 1.0`, `*`). +- The `predicates` field uses function-call syntax (`depends-on(...)`, `shell(...)`, `path_exists(...)`, `env(...)`), combined with `not`, `any`, and `all`. When a skill group declares `source = "crate"`, Symposium fetches the matched crate's source (from the local path, the cargo registry cache, or crates.io), reads `[package.metadata.symposium]` from its `Cargo.toml` to locate the skills, and follows crate-to-crate redirects. See the [predicates reference](https://symposium.dev/reference/predicates.html). diff --git a/md/SUMMARY.md b/md/SUMMARY.md index 59d4a66e..cc8bb1ee 100644 --- a/md/SUMMARY.md +++ b/md/SUMMARY.md @@ -53,7 +53,7 @@ - [Plugin definition](./reference/plugin-definition.md) - [Symposium hook events](./reference/hook-events.md) - [Skill definition](./reference/skill-definition.md) - - [Crate predicates](./reference/crate-predicates.md) + - [Dependency predicates (`depends-on`)](./reference/depends-on.md) - [Predicates](./reference/predicates.md) - [Contribution guide](./design/welcome.md) - [Tenets](./design/tenets.md) diff --git a/md/crate-authors/authoring-a-plugin.md b/md/crate-authors/authoring-a-plugin.md index 8a6fa734..333d046a 100644 --- a/md/crate-authors/authoring-a-plugin.md +++ b/md/crate-authors/authoring-a-plugin.md @@ -9,10 +9,10 @@ Every plugin starts with a `SYMPOSIUM.toml` manifest uploaded to the [central re ```toml # `my-crate/SYMPOSIUM.toml` on the symposium-dev/recommendations repository name = "my-crate" -crates = ["my-crate"] +depends-on = ["my-crate"] ``` -The `crates` field controls when the plugin is active — it will only load for projects that depend on the listed crates. Use `["*"]` to apply to all projects. +The `depends-on` field controls when the plugin is active — it will only load for projects that depend on the listed crates. Use `["*"]` to apply to all projects. See the [plugin definition reference](../reference/plugin-definition.md) for the full manifest schema. @@ -63,7 +63,7 @@ my-crate/ ```toml # `my-crate/SYMPOSIUM.toml` on the symposium-dev/recommendations repository name = "my-crate" -crates = ["my-crate"] +depends-on = ["my-crate"] [[skills]] source = "crate" @@ -102,19 +102,19 @@ And point the manifest at the local directory: ```toml name = "my-crate" -crates = ["my-crate"] +depends-on = ["my-crate"] [[skills]] source.path = "." ``` -Standalone skills **must** include `crates` in their frontmatter so Symposium knows which crate they apply to: +Standalone skills **must** include `depends-on` in their frontmatter so Symposium knows which crate they apply to: ```markdown --- name: widgetlib-basics description: Basic guidance for widgetlib usage -crates: widgetlib=1.0 +depends-on: widgetlib=1.0 --- Guidance body here. diff --git a/md/crate-authors/writing-a-hook-handler.md b/md/crate-authors/writing-a-hook-handler.md index 565d9c7e..e64909d1 100644 --- a/md/crate-authors/writing-a-hook-handler.md +++ b/md/crate-authors/writing-a-hook-handler.md @@ -60,7 +60,7 @@ In your `SYMPOSIUM.toml`, reference the built binary as a hook command: ```toml name = "my-crate" -crates = ["my-crate"] +depends-on = ["my-crate"] [[hooks]] name = "check-usage" diff --git a/md/design/important-flows.md b/md/design/important-flows.md index 1a831195..594122d3 100644 --- a/md/design/important-flows.md +++ b/md/design/important-flows.md @@ -6,7 +6,7 @@ This section describes the logic of each `cargo agents` command. When a skill group uses `source = "crate"`, the sync flow takes an additional path: -1. `predicate::union_matched_crates()` resolves plugin-level and group-level predicates against the workspace to produce a set of concrete crate name/version pairs. +1. `predicate::union_matched_packages()` resolves plugin-level and group-level predicates against the workspace to produce a set of concrete crate name/version pairs. 2. For each crate in the set, `RustCrateFetch` fetches the source — checking path overrides (for local path deps), then the cargo registry cache, then crates.io. 3. `crate_metadata::parse_crate_metadata()` reads `[package.metadata.symposium]` from the crate's `Cargo.toml`: - **No metadata** — fall back to the default `skills/` subdirectory. @@ -15,7 +15,7 @@ When a skill group uses `source = "crate"`, the sync flow takes an additional pa - **`crate = { name, version? }` entries** — redirect: fetch the target crate and follow its metadata recursively (with cycle detection and a depth limit of 10). 4. `discover_skills()` scans each resolved directory for `SKILL.md` files. -The key code paths are in `skills.rs` (`load_crate_skills`, `fetch_and_resolve_skills`), `crate_metadata.rs` (`parse_crate_metadata`), `predicate.rs` (`matched_crates`, `union_matched_crates`), and `crate_sources/mod.rs` (`RustCrateFetch`, `WorkspaceCrate`). +The key code paths are in `skills.rs` (`load_crate_skills`, `fetch_and_resolve_skills`), `crate_metadata.rs` (`parse_crate_metadata`), `predicate.rs` (`witness`, `union_matched_packages`), and `crate_sources/mod.rs` (`RustCrateFetch`, `WorkspaceCrate`). ## Help rendering @@ -33,7 +33,7 @@ clap's auto help flag and help subcommand are disabled in `cli::Cli`; `--help`/` When the user runs `cargo agents ` for a name not built into the binary, clap's `allow_external_subcommands` routes it to `Commands::External(argv)`. 1. The binary (or library `cli::run`) calls `subcommand_dispatch::dispatch_external(sym, cwd, argv)`. -2. `find_subcommand` walks the plugin registry. For each plugin it applies the plugin-level `crates` predicate against the workspace, then looks up `argv[0]` in `plugin.subcommands`. If the entry has its own `crates` predicate, that must also match. Two or more matches → error. +2. `find_subcommand` walks the plugin registry. For each plugin it applies the plugin-level `depends-on` predicate against the workspace, then looks up `argv[0]` in `plugin.subcommands`. If the entry has its own `depends-on` predicate, that must also match. Two or more matches → error. 3. The matched subcommand's `command` field names an `Installation` on the same plugin. `installation::resolve_runnable` acquires the source if any, runs `install_commands`, and picks the `Runnable` (`Exec` for binaries, `Script` for shell scripts). 4. The child is spawned with stdio inherited. Its exit code is collapsed to a `u8` — the binary wraps it in `ExitCode::from`; the library treats non-zero as an error so the test harness can assert on success/failure. diff --git a/md/design/module-structure.md b/md/design/module-structure.md index 15319b9d..907921fa 100644 --- a/md/design/module-structure.md +++ b/md/design/module-structure.md @@ -29,7 +29,7 @@ Two entry points: `sync(sym, cwd)` for standalone CLI use (creates its own `Work Scans configured plugin source directories for TOML manifests and parses them into `Plugin` structs. Validation here turns the raw TOML into: - `Installation` entries (optional `source`, optional `executable`/`script`, optional `args`, plus `requirements` and `install_commands`) collected on `Plugin.installations`. Inline installation references on hooks or other installations are *promoted* into synthetic `Installation` entries with derived names (`` for an inline `command`, `__req_` for an inline requirement), so all references in the validated form are plain names. - `Hook` entries with `command: String` (the name of an `Installation`) plus optional hook-level `executable` / `script` / `args`. Validation guarantees at most one of `executable`/`script` is set across hook + installation, and at most one layer sets `args`. -- `SkillGroup` and `PluginMcpServer` entries whose `crates` sugar and `predicates` list are merged into one runtime `PredicateSet`. Skill group `source` syntax is deserialized as raw string/table forms, then validated into `PluginSource`. +- `SkillGroup` and `PluginMcpServer` entries whose `depends-on` sugar and `predicates` list are merged into one runtime `PredicateSet`. Skill group `source` syntax is deserialized as raw string/table forms, then validated into `PluginSource`. Also discovers standalone `SKILL.md` files not wrapped in a plugin. Returns a `PluginRegistry` — a table of contents that doesn't load skill content. @@ -51,16 +51,16 @@ Parses `[package.metadata.symposium]` from crate `Cargo.toml` files. Crate autho ### `predicate.rs` — unified activation predicates -Defines one `Predicate` enum covering both crate-graph matching and runtime/environment gating, plus `PredicateSet` (a list ANDed together) and `PredicateContext` (the workspace crate list it evaluates against). Two surface syntaxes lower to the same tree: +Defines one `Predicate` enum covering both dependency-graph matching and runtime/environment gating, plus `PredicateSet` (a list ANDed together) and `PredicateContext` (the workspace dependency list it evaluates against). Two surface syntaxes lower to the same tree: -- The **`crates`** field uses crate-atom syntax (`serde`, `serde>=1.0`, `*`) and lowers, via `CrateList`, to `crate(...)` / `crate(*)` predicates OR-combined into a single `any(...)` that is appended to the same list. So `crates` is sugar — there is no separate crate-predicate type. -- The **`predicates`** field uses function-call syntax: `crate()`, `shell()` (verbatim arg, `sh -c`, exit 0 holds), `path_exists()` (disk, then `$PATH` for bare names), `env([=])`, and the combinators `not(

)`, `any(

, …)`, `all(

, …)`. +- The **`depends-on`** field uses dependency-atom syntax (`serde`, `serde>=1.0`, `*`) and lowers, via `DependsOnList`, to `depends-on(...)` / `depends-on(*)` predicates OR-combined into a single `any(...)` that is appended to the same list. So `depends-on` is sugar — there is no separate dependency-predicate type. +- The **`predicates`** field uses function-call syntax: `depends-on()`, `shell()` (verbatim arg, `sh -c`, exit 0 holds), `path_exists()` (disk, then `$PATH` for bare names), `env([=])`, and the combinators `not(

)`, `any(

, …)`, `all(

, …)`. The retired `crate(...)` spelling is rejected with a migration hint, as are the old `crates` fields. -Each gated struct (plugin, skill group, skill, hook, MCP server, subcommand) stores a single merged `predicates: PredicateSet`. Evaluation is `PredicateSet::evaluate(ctx) -> bool`. For `source = "crate"`, `witness` / `union_matched_crates` return the concrete crates that participate in a *satisfying* evaluation (the fetch set): `crate(c)` contributes `c` when present, `any` unions its true children, `all` unions all children when all hold, and `not` contributes nothing. `collect_crate_names` (crates.io validation) walks all positions regardless. Plugin/group/skill/MCP predicates are evaluated at sync time; hook dispatch evaluates the plugin-level set (so a plugin's `crates` now gate its hooks) plus the hook-level set. Hook dispatch threads in the workspace crate list, but resolves it (running cargo) only when some plugin- or hook-level predicate references a *concrete* `crate(...)` — wildcard and env/shell/path predicates dispatch without a cargo query. See the [predicates reference](../reference/predicates.md). +Each gated struct (plugin, skill group, skill, hook, MCP server, subcommand) stores a single merged `predicates: PredicateSet`. Evaluation is `PredicateSet::evaluate(ctx) -> bool`. For `source = "crate"`, `witness` / `union_matched_packages` return the concrete crates that participate in a *satisfying* evaluation (the fetch set): `depends-on(c)` contributes `c` when present, `any` unions its true children, `all` unions all children when all hold, and `not` contributes nothing. `collect_dep_names` (crates.io validation) walks all positions regardless. Plugin/group/skill/MCP predicates are evaluated at sync time; hook dispatch evaluates the plugin-level set (so a plugin's `depends-on` now gates its hooks) plus the hook-level set. Hook dispatch threads in the workspace crate list, but resolves it (running cargo) only when some plugin- or hook-level predicate references a *concrete* `depends-on(...)` — wildcard and env/shell/path predicates dispatch without a cargo query. See the [predicates reference](../reference/predicates.md). ### `skills.rs` — skill resolution and matching -Given a `PluginRegistry` and workspace dependencies, this module resolves skill group sources (fetching from git if needed), discovers `SKILL.md` files, and evaluates crate predicates at each level (plugin, group, skill) to determine which skills apply. For `source = "crate"` groups, resolves predicates to a matched crate set, fetches each crate's source via `RustCrateFetch`, reads `[package.metadata.symposium]` to determine skill paths, and follows redirects recursively with cycle detection and a depth limit of 10. +Given a `PluginRegistry` and workspace dependencies, this module resolves skill group sources (fetching from git if needed), discovers `SKILL.md` files, and evaluates dependency predicates at each level (plugin, group, skill) to determine which skills apply. For `source = "crate"` groups, resolves predicates to a matched crate set, fetches each crate's source via `RustCrateFetch`, reads `[package.metadata.symposium]` to determine skill paths, and follows redirects recursively with cycle detection and a depth limit of 10. Each applicable skill carries a `SkillOrigin` describing *where its bytes live*, used at sync time for dedup and install-path disambiguation. What matters for identity is the on-disk location of the skill, not which plugin manifest pointed at it — two plugins in the same source pointing at the same skill bundle dedupe. - `Crate { name, version }` — from a crate-source resolution (`source = "crate"`). Two `Crate` origins with the same `(name, version)` are the same logical skill, regardless of which plugin pointed at them. @@ -71,7 +71,7 @@ Each applicable skill carries a `SkillOrigin` describing *where its bytes live*, ### `subcommand_dispatch.rs` — plugin-vended subcommands -Routes the `Commands::External` arm of clap's `allow_external_subcommands`. `find_subcommand` walks the `PluginRegistry`, applying plugin-level and subcommand-level crate predicates against the workspace, and returns the matched `(Plugin, Subcommand)` (or an error if more than one plugin claims the name). `dispatch_external` then looks up the named `Installation`, resolves it via `installation::resolve_runnable`, and spawns the child with stdio inherited — propagating the exit code as a `u8` so callers can convert to `ExitCode` (binary) or treat non-zero as an error (library). `applicable_subcommands` is the shared iterator over workspace-applicable plugin subcommands, reused by help rendering. +Routes the `Commands::External` arm of clap's `allow_external_subcommands`. `find_subcommand` walks the `PluginRegistry`, applying plugin-level and subcommand-level dependency predicates against the workspace, and returns the matched `(Plugin, Subcommand)` (or an error if more than one plugin claims the name). `dispatch_external` then looks up the named `Installation`, resolves it via `installation::resolve_runnable`, and spawns the child with stdio inherited — propagating the exit code as a `u8` so callers can convert to `ExitCode` (binary) or treat non-zero as an error (library). `applicable_subcommands` is the shared iterator over workspace-applicable plugin subcommands, reused by help rendering. ### `help_render.rs` — `--help` rendering diff --git a/md/design/subcommands.md b/md/design/subcommands.md index b19e6525..1c150429 100644 --- a/md/design/subcommands.md +++ b/md/design/subcommands.md @@ -18,7 +18,7 @@ This means a plugin author writes installation logic once and shares it across h ```toml name = "demo-plugin" -crates = ["example-crate"] +depends-on = ["example-crate"] [[installations]] name = "example-tool" @@ -38,7 +38,7 @@ command = "example-tool" | `description` | string | yes | Shown in `cargo agents --help`. Capped at 1024 chars. | | `audience` | `"humans"` \| `"agents"` | no, defaults to `"agents"` | Controls grouping in `cargo agents --help`. | | `command` | string or table | yes | A string names an `[[installations]]` entry; a table is an inline installation, promoted to a synthetic entry named after the subcommand. Same shape as `[[hooks]].command`. | -| `crates` | string or array | no | Subcommand-level crate predicate, AND-combined with the plugin-level `crates`. | +| `depends-on` | string or array | no | Subcommand-level dependency predicate, AND-combined with the plugin-level `depends-on`. | Reserved names that cannot be used as subcommand keys: `init`, `sync`, `hook`, `plugin`, `crate-info`, `help`. A plugin cannot shadow a built-in. @@ -61,7 +61,7 @@ The inline table is promoted to a synthetic installation named after the subcomm Symposium does not own the subcommand's argument grammar. The plugin's binary owns its own `--help`, validation, and exit codes. What symposium contributes is mechanical: 1. Name registration and lookup. -2. Workspace-aware filtering (the subcommand only appears for projects matching the plugin's crate predicates). +2. Workspace-aware filtering (the subcommand only appears for projects matching the plugin's dependency predicates). 3. A short description shown in `cargo agents --help`. 4. Resolution of `command` through the installation pipeline to a concrete `(executable, base_args)`. 5. Forwarding the user's trailing CLI args verbatim, appended after the installation's `args`. @@ -73,7 +73,7 @@ This boundary keeps the manifest small, keeps plugins authoritative about their `cargo-agents`'s top-level CLI uses clap's `allow_external_subcommands`: unknown subcommands are not errors but are routed to a catch-all variant. The binary then: 1. Loads the plugin registry and the active workspace's crates. -2. Walks active plugins for one whose `subcommands` map contains the typed name *and* whose subcommand-level `crates` predicate (if any) also matches. +2. Walks active plugins for one whose `subcommands` map contains the typed name *and* whose subcommand-level `depends-on` predicate (if any) also matches. 3. Resolves the subcommand's `command` through the installation pipeline — acquiring the binary if it isn't already cached, running any `install_commands`, processing `requirements`. 4. Execs the resolved `(executable, base_args ++ user_args)`, inheriting stdio. 5. Returns the child's exit code as the `cargo agents` exit code. A signal-killed child becomes a generic failure. @@ -88,9 +88,9 @@ If no plugin matches the typed name, dispatch fails with a clear error pointing Plugin filtering is workspace-aware in two places: help rendering and dispatch. -**Inside a Cargo workspace.** Symposium reads the workspace's resolved dependencies. A subcommand appears in `cargo agents --help` and is dispatchable only if both the plugin-level and subcommand-level `crates` predicates match. Built-in subcommands always appear. +**Inside a Cargo workspace.** Symposium reads the workspace's resolved dependencies. A subcommand appears in `cargo agents --help` and is dispatchable only if both the plugin-level and subcommand-level `depends-on` predicates match. Built-in subcommands always appear. -**Outside a Cargo workspace** (no discoverable `Cargo.toml` upward). Only built-ins and plugins with `crates = ["*"]` appear. Invoking a crate-specific subcommand from outside a workspace produces an error explaining which crate it needs. +**Outside a Cargo workspace** (no discoverable `Cargo.toml` upward). Only built-ins and plugins with `depends-on = ["*"]` appear. Invoking a crate-specific subcommand from outside a workspace produces an error explaining which crate it needs. This rule keeps `cargo agents --help` outside a workspace limited to globally-applicable commands, rather than listing every installed plugin. @@ -121,7 +121,7 @@ The hint shares `SessionStart`'s `additionalContext` with the [update nudge](./h ## Conflict resolution -Two plugins may declare the same subcommand name. Rather than silently picking one, dispatch fails with an error listing every plugin that defined the name, leaving the user to disambiguate (typically by tightening one of the plugin's `crates` predicates or removing one of the plugin sources). +Two plugins may declare the same subcommand name. Rather than silently picking one, dispatch fails with an error listing every plugin that defined the name, leaving the user to disambiguate (typically by tightening one of the plugin's `depends-on` predicates or removing one of the plugin sources). The strict-error stance trades silence for clarity: subcommand names tend to mirror crate names (which are unique on crates.io), so a collision usually signals a real configuration mistake rather than an intended override. diff --git a/md/design/sync-agent-flow.md b/md/design/sync-agent-flow.md index cfa0aca8..076cb3cd 100644 --- a/md/design/sync-agent-flow.md +++ b/md/design/sync-agent-flow.md @@ -10,7 +10,7 @@ Scans workspace dependencies, installs applicable skills into agent directories, 3. **Scan dependencies** — read the full dependency graph from the workspace. -4. **Match skills to dependencies** — for each plugin, parse `SKILL.md` YAML frontmatter, reject malformed or non-string metadata, warn about skipped invalid skills, then evaluate skill group crate predicates and individual skill `crates` frontmatter against the workspace dependencies. +4. **Match skills to dependencies** — for each plugin, parse `SKILL.md` YAML frontmatter, reject malformed or non-string metadata, warn about skipped invalid skills, then evaluate skill group dependency predicates and individual skill `depends-on` frontmatter against the workspace dependencies. 5. **Install skills per agent** — for each configured agent: - Copy applicable `SKILL.md` files into the agent's expected skill directory. diff --git a/md/reference/crate-predicates.md b/md/reference/crate-predicates.md deleted file mode 100644 index 3a4b016c..00000000 --- a/md/reference/crate-predicates.md +++ /dev/null @@ -1,73 +0,0 @@ -# Crate predicates - -Crate predicates control when plugins, skill groups, and individual skills are active. A predicate matches against a **workspace's direct dependency set** — not against individual crates in isolation. - -The `crates` field is shorthand: `crates = ["serde", "tokio"]` lowers to a single `any(crate(serde), crate(tokio))` predicate and is merged into the same list as the [`predicates`](./predicates.md) field (ANDed together). Everything below describes the crate-atom syntax `crates` accepts; the equivalent `crate()` predicate is also usable directly in `predicates`. - -## Predicate syntax - -A crate predicate is a crate name with an optional version requirement. - -Examples: - -- `serde` -- `serde>=1.0` -- `tokio^1.40` -- `regex<2.0` -- `serde=1.0` -- `serde==1.0.219` -- `*` - -Semantics: - -- bare crate name: matches if the workspace has this crate as a direct dependency (any version) -- `>=`, `<=`, `>`, `<`, `^`, `~`: standard semver operators applied to the workspace's version of the crate -- `=1.0`: compatible-version matching, equivalent to `^1.0` -- `==1.0.219`: exact-version matching -- `*`: wildcard — always matches, even a workspace with zero dependencies - -Predicates match against **direct** workspace dependencies only, not transitive ones. - -## Usage in different contexts - -### Plugin manifests (TOML) - -The `crates` field accepts an array of predicate strings: - -- `crates = ["serde"]` -- `crates = ["serde", "tokio>=1.40"]` -- `crates = ["*"]` (wildcard — always active) - -### Skill frontmatter (YAML) - -The `crates` field uses comma-separated values: - -- `crates: serde` -- `crates: serde, tokio>=1.40` - -## Matching behavior - -A `crates` list matches if *at least one* predicate in the list matches the workspace. The wildcard `*` always matches — even a workspace with zero dependencies. - -If there are multiple `crates` declarations in scope, all of them must match (AND composition). For example with skills, `crates` predicates can appear at three distinct levels: - -* If a [plugin](./plugin-definition.md) defines `crates` at the top-level, it must match before any other plugin contents will be considered. -* If a skill-group within a plugin defines `crates`, that predicate must match before the skills themselves will be fetched. -* If the skills define `crates` in their front-matter, those crates must match before the skills will be added to the project. - -## Matched crate set - -When a skill group uses `source = "crate"`, predicates serve a second purpose beyond filtering: they determine **which crate sources to fetch**. - -Each non-wildcard predicate that matches a workspace dependency contributes that dependency's name and version to the *matched crate set*. Symposium then fetches the source for each crate in the set and looks for skills inside it. - -- `"serde"` against a workspace with `serde 1.0.210` → matched set: `{serde@1.0.210}` -- `"serde"` against a workspace without serde → no match, plugin skipped -- `"*"` → matches, but contributes no concrete crates to the set -- `["serde", "tokio"]` with both in workspace → `{serde@1.0.210, tokio@1.38.0}` - -Predicates from both the plugin level and the group level are unioned together to form the matched set. - -Because wildcards contribute no concrete crates, **at least one non-wildcard predicate must be present** (at either the plugin or group level) when using crate-sourced skills. A manifest with only wildcards and `source = "crate"` is rejected at parse time. - -When the gate uses combinators (`any`, `all`, `not`) or mixes `crate(...)` with non-crate predicates, the matched set generalizes to the predicate's **witness** — the concrete crates that participate in a satisfying evaluation. See [Crate-sourced skills and the witness](./predicates.md#crate-sourced-skills-and-the-witness). diff --git a/md/reference/depends-on.md b/md/reference/depends-on.md new file mode 100644 index 00000000..ab9ccc3f --- /dev/null +++ b/md/reference/depends-on.md @@ -0,0 +1,77 @@ +# Dependency predicates (`depends-on`) + +Dependency predicates control when plugins, skill groups, and individual skills are active. A predicate matches against a **workspace's direct dependency set** — not against individual packages in isolation. Today the dependency set is the workspace's cargo dependency graph; a `depends-on` atom matches a direct dependency by name. + +The `depends-on` field is shorthand: `depends-on = ["serde", "tokio"]` lowers to a single `any(depends-on(serde), depends-on(tokio))` predicate and is merged into the same list as the [`predicates`](./predicates.md) field (ANDed together). Everything below describes the dependency-atom syntax `depends-on` accepts; the equivalent `depends-on()` predicate is also usable directly in `predicates`. + +## Predicate syntax + +A dependency atom is a package name with an optional version requirement. + +Examples: + +- `serde` +- `serde>=1.0` +- `tokio^1.40` +- `regex<2.0` +- `serde=1.0` +- `serde==1.0.219` +- `*` + +Semantics: + +- bare name: matches if the workspace has this package as a direct dependency (any version) +- `>=`, `<=`, `>`, `<`, `^`, `~`: standard semver operators applied to the workspace's version of the package +- `=1.0`: compatible-version matching, equivalent to `^1.0` +- `==1.0.219`: exact-version matching +- `*`: wildcard — always matches, even a workspace with zero dependencies + +Predicates match against **direct** workspace dependencies only, not transitive ones. + +## Usage in different contexts + +### Plugin manifests (TOML) + +The `depends-on` field accepts an array of atom strings: + +- `depends-on = ["serde"]` +- `depends-on = ["serde", "tokio>=1.40"]` +- `depends-on = ["*"]` (wildcard — always active) + +### Skill frontmatter (YAML) + +The `depends-on` field uses comma-separated values: + +- `depends-on: serde` +- `depends-on: serde, tokio>=1.40` + +## Matching behavior + +A `depends-on` list matches if *at least one* atom in the list matches the workspace. The wildcard `*` always matches — even a workspace with zero dependencies. + +If there are multiple `depends-on` declarations in scope, all of them must match (AND composition). For example with skills, `depends-on` predicates can appear at three distinct levels: + +* If a [plugin](./plugin-definition.md) defines `depends-on` at the top-level, it must match before any other plugin contents will be considered. +* If a skill-group within a plugin defines `depends-on`, that predicate must match before the skills themselves will be fetched. +* If the skills define `depends-on` in their front-matter, those dependencies must match before the skills will be added to the project. + +## Matched crate set + +When a skill group uses `source = "crate"`, predicates serve a second purpose beyond filtering: they determine **which crate sources to fetch**. + +Each non-wildcard atom that matches a workspace dependency contributes that dependency's name and version to the *matched crate set*. Symposium then fetches the source for each crate in the set and looks for skills inside it. + +- `"serde"` against a workspace with `serde 1.0.210` → matched set: `{serde@1.0.210}` +- `"serde"` against a workspace without serde → no match, plugin skipped +- `"*"` → matches, but contributes no concrete crates to the set +- `["serde", "tokio"]` with both in workspace → `{serde@1.0.210, tokio@1.38.0}` + +Predicates from both the plugin level and the group level are unioned together to form the matched set. + +Because wildcards contribute no concrete crates, **at least one non-wildcard atom must be present** (at either the plugin or group level) when using crate-sourced skills. A manifest with only wildcards and `source = "crate"` is rejected at parse time. + +When the gate uses combinators (`any`, `all`, `not`) or mixes `depends-on(...)` with other predicates, the matched set generalizes to the predicate's **witness** — the concrete crates that participate in a satisfying evaluation. See [Crate-sourced skills and the witness](./predicates.md#crate-sourced-skills-and-the-witness). + +## Migration from `crates` + +`depends-on` replaces the former `crates` field and `crate(...)` predicate (renamed as part of the [registry-centric plugin distribution RFD](../rfds/registry-centric-plugins/README.md), which generalizes dependency matching beyond cargo). The old spellings are rejected at parse time with a migration hint — the atom syntax itself is unchanged, so migrating is a mechanical rename. diff --git a/md/reference/plugin-definition.md b/md/reference/plugin-definition.md index 745dd7ac..6ca0f397 100644 --- a/md/reference/plugin-definition.md +++ b/md/reference/plugin-definition.md @@ -22,7 +22,7 @@ where `myplugin/SYMPOSIUM.toml` is as follows: ```toml name = "example" -crates = ["*"] +depends-on = ["*"] [[skills]] source.path = "skills" @@ -33,7 +33,7 @@ source.path = "skills" | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | string | yes | Plugin name. Used in logs and CLI output. | -| `crates` | string or array | no | Which crates this plugin applies to. Use `["*"]` for all crates. See [Plugin-level filtering](#plugin-level-filtering). | +| `depends-on` | string or array | no | Which crates this plugin applies to. Use `["*"]` for all crates. See [Plugin-level filtering](#plugin-level-filtering). | | `predicates` | array of strings | no | Predicates (`crate`, `shell`, `path_exists`, `env`, `not`, `any`, `all`) that must all hold for the plugin to apply. See [Predicates](./predicates.md). | | `installations` | array of tables | no | Named installation declarations (`[[installations]]`). Hooks reference these by name. See [Installations](#installations). | | `skills` | array of tables | no | Skill groups (`[[skills]]`). | @@ -41,18 +41,18 @@ source.path = "skills" | `predicate` | array of tables | no | Custom predicate definitions (`[[predicate]]`). See [Custom predicates](#predicate). | | `mcp_servers` | array of tables | no | MCP server registrations (`[[mcp_servers]]`). | -**Note**: Every plugin must reference at least one crate somewhere — at the plugin level, in `[[skills]]` groups, or in `[[mcp_servers]]` entries — via a `crates` list or a `crate(...)` [predicate](./predicates.md). Plugins without any crate targeting will fail validation. +**Note**: Every plugin must reference at least one crate somewhere — at the plugin level, in `[[skills]]` groups, or in `[[mcp_servers]]` entries — via a `depends-on` list or a `depends-on(...)` [predicate](./predicates.md). Plugins without any crate targeting will fail validation. ## Plugin-level filtering -The top-level `crates` field controls when the entire plugin is active: +The top-level `depends-on` field controls when the entire plugin is active: ```toml name = "my-plugin" -crates = ["serde", "tokio"] # Only active in projects using serde OR tokio +depends-on = ["serde", "tokio"] # Only active in projects using serde OR tokio # OR use wildcard to always apply -crates = ["*"] +depends-on = ["*"] ``` Plugin-level filtering is combined with skill group filtering using AND logic — both must match for skills to be available. @@ -63,7 +63,7 @@ Each `[[skills]]` entry declares a group of skills. | Field | Type | Description | |-------|------|-------------| -| `crates` | string or array | Which crates this group advises on. Accepts a single string (`"serde"`) or array (`["serde", "tokio>=1.0"]`). See [Crate predicates](./crate-predicates.md) for syntax. | +| `depends-on` | string or array | Which crates this group advises on. Accepts a single string (`"serde"`) or array (`["serde", "tokio>=1.0"]`). See [Crate predicates](./depends-on.md) for syntax. | | `predicates` | array of strings | Predicates (`crate`, `shell`, `path_exists`, `env`, `not`, `any`, `all`) that must all hold for the group to install. See [Predicates](./predicates.md). | | `source.path` | string | Local directory containing skill subdirectories. Resolved relative to the manifest file. | | `source.git` | string | GitHub URL pointing to a directory in a repository (e.g., `https://github.com/org/repo/tree/main/skills`). Symposium downloads the tarball, extracts the subdirectory, and caches it. | @@ -79,13 +79,13 @@ This is the recommended way for crate authors to ship skills alongside their cra ```toml name = "serde-plugin" -crates = ["serde"] +depends-on = ["serde"] [[skills]] source = "crate" ``` -At least one non-wildcard crate predicate must be present (at either the plugin or group level) so that Symposium knows which crate sources to fetch. See [Matched crate set](./crate-predicates.md#matched-crate-set) for details. +At least one non-wildcard crate predicate must be present (at either the plugin or group level) so that Symposium knows which crate sources to fetch. See [Matched crate set](./depends-on.md#matched-crate-set) for details. #### Skill layout in crate source @@ -108,7 +108,7 @@ If your plugin lists multiple crates, then skills will be loaded from whichever ```toml name = "my-plugin" -crates = ["foo", "bar", "baz"] +depends-on = ["foo", "bar", "baz"] [[skills]] source = "crate" @@ -128,22 +128,22 @@ crate = { name = "dial9-viewer" } This means the plugin just needs `source = "crate"` and the crate itself controls where skills are fetched from. Redirects are followed recursively (with cycle detection and a depth limit of 10). -#### Semantics of `crates` predicates at multiple levels +#### Semantics of `depends-on` predicates at multiple levels -When you apply the `crates` predicate at multiple levels, all levels must match. This can be used to narrow the set of crates that have skills versus the set that activates your plugin overall. +When you apply the `depends-on` predicate at multiple levels, all levels must match. This can be used to narrow the set of crates that have skills versus the set that activates your plugin overall. ```toml name = "my-plugin" # Any of these crates activates the plugin -crates = ["foo", "bar", "baz"] +depends-on = ["foo", "bar", "baz"] [[skills]] -crates = ["foo", "bar"] # ... but this block applies only to "foo" and "bar" +depends-on = ["foo", "bar"] # ... but this block applies only to "foo" and "bar" source = "crate" # ...which get their skills from their sources [[skills]] -crates = ["baz"] # ... this block applies to "baz" +depends-on = ["baz"] # ... this block applies to "baz" source = "crate" # ... baz's Cargo.toml metadata controls where skills come from ``` @@ -492,7 +492,7 @@ The argument is trimmed of leading/trailing whitespace before being passed. An e ### Witness output (stdout JSON) -On success (exit 0), the command may write a JSON object to stdout. If present and valid, the `selectedCrates` field drives `source = "crate"` skill resolution — the named crates are fetched for skills, just as if they'd been matched by a `crate(...)` predicate. +On success (exit 0), the command may write a JSON object to stdout. If present and valid, the `selectedCrates` field drives `source = "crate"` skill resolution — the named crates are fetched for skills, just as if they'd been matched by a `depends-on(...)` predicate. ```json { @@ -531,7 +531,7 @@ env = [] | Field | Type | Description | |-------|------|-------------| | `name` | string | Server name as it appears in the agent's MCP config. | -| `crates` | string or array | Which crates this server applies to. Optional if plugin has top-level `crates`. | +| `depends-on` | string or array | Which crates this server applies to. Optional if plugin has top-level `depends-on`. | | `predicates` | array of strings | Predicates (`crate`, `shell`, `path_exists`, `env`, `not`, `any`, `all`) that must all hold for the server to register. See [Predicates](./predicates.md). | | `command` | string | Path to the server binary. | | `args` | array of strings | Arguments passed to the binary. | @@ -553,7 +553,7 @@ headers = [] |-------|------|-------------| | `type` | string | Must be `"http"`. | | `name` | string | Server name as it appears in the agent's MCP config. | -| `crates` | string or array | Which crates this server applies to. Optional if plugin has top-level `crates`. | +| `depends-on` | string or array | Which crates this server applies to. Optional if plugin has top-level `depends-on`. | | `url` | string | HTTP endpoint URL. | | `headers` | array of objects | HTTP headers to set when making requests. | @@ -571,7 +571,7 @@ headers = [] |-------|------|-------------| | `type` | string | Must be `"sse"`. | | `name` | string | Server name as it appears in the agent's MCP config. | -| `crates` | string or array | Which crates this server applies to. Optional if plugin has top-level `crates`. | +| `depends-on` | string or array | Which crates this server applies to. Optional if plugin has top-level `depends-on`. | | `url` | string | SSE endpoint URL. | | `headers` | array of objects | HTTP headers to set when making requests. | @@ -600,16 +600,16 @@ All supported agents have MCP server configuration. Symposium handles the format ```toml name = "widgetlib" -crates = ["widgetlib"] +depends-on = ["widgetlib"] # Skills shipped inside the widgetlib crate source (in skills/) [[skills]] -crates = ["widgetlib"] +depends-on = ["widgetlib"] source = "crate" # Additional skills hosted in a git repo [[skills]] -crates = ["widgetlib=1.0"] +depends-on = ["widgetlib=1.0"] source.git = "https://github.com/org/widgetlib/tree/main/symposium/serde-skills" [[hooks]] diff --git a/md/reference/plugin-source.md b/md/reference/plugin-source.md index 0a2af542..fb95996e 100644 --- a/md/reference/plugin-source.md +++ b/md/reference/plugin-source.md @@ -7,7 +7,7 @@ A **plugin source** is a directory or repository containing plugins and standalo Symposium scans a plugin source recursively to find plugins and standalone skills: * A [plugin](./plugin-definition.md) is a directory that contains a `SYMPOSIUM.toml` file; -* A [standalone skill](./skill-definition.md) is a directory that contains a `SKILL.md` and does not contain a `SYMPOSIUM.toml` file. Standalone skills must have `crates` metadata in their frontmatter. +* A [standalone skill](./skill-definition.md) is a directory that contains a `SKILL.md` and does not contain a `SYMPOSIUM.toml` file. Standalone skills must have `depends-on` metadata in their frontmatter. **We do not allow plugins or standalone skills to be nested within one another.** When we find a directory that is either a plugin or a skill, we do not search its contents any further. diff --git a/md/reference/predicates.md b/md/reference/predicates.md index c196aead..7cd01241 100644 --- a/md/reference/predicates.md +++ b/md/reference/predicates.md @@ -1,18 +1,18 @@ # Predicates -A **predicate** decides whether a plugin, skill group, skill, hook, MCP server, or subcommand is active, evaluated against the workspace's crate graph and the live environment. There is one predicate model, written two ways: +A **predicate** decides whether a plugin, skill group, skill, hook, MCP server, or subcommand is active, evaluated against the workspace's dependency graph and the live environment. There is one predicate model, written two ways: -- The **`crates`** field uses crate-atom syntax (see [crate predicates](./crate-predicates.md)) and is **sugar**: `crates = ["serde", "tokio"]` lowers to a single `any(crate(serde), crate(tokio))` predicate. +- The **`depends-on`** field uses dependency-atom syntax (see [dependency predicates](./depends-on.md)) and is **sugar**: `depends-on = ["serde", "tokio"]` lowers to a single `any(depends-on(serde), depends-on(tokio))` predicate. - The **`predicates`** field uses the function-call syntax below. -Both fields are merged into one list that is ANDed together, so `crates` and `predicates` compose with **AND**. A `crate(...)` predicate is available in `predicates` too — `crates` just makes the common case terse. +Both fields are merged into one list that is ANDed together, so `depends-on` and `predicates` compose with **AND**. A `depends-on(...)` predicate is available in `predicates` too — the field just makes the common case terse. The available predicate functions are: | Predicate | Holds when | |-----------|------------| -| `crate()` / `crate()` | A workspace dependency named `` is present (and its version satisfies ``, e.g. `crate(serde>=1.0)`). | -| `crate(*)` | Any workspace matches (even one with zero dependencies). The lowered form of `*`. | +| `depends-on()` / `depends-on()` | A workspace dependency named `` is present (and its version satisfies ``, e.g. `depends-on(serde>=1.0)`). | +| `depends-on(*)` | Any workspace matches (even one with zero dependencies). The lowered form of `*`. | | `shell()` | `` run via `sh -c` exits `0`. Any other exit (including spawn failure) fails. | | `path_exists()` | `` resolves to an existing path. An argument with a path separator is checked on the filesystem (cwd-relative or absolute). A bare name with no separator is checked against the cwd and then searched on `$PATH`, so it matches either a local entry (`path_exists(.git)`) or an installed binary (`path_exists(rg)`). | | `env()` | The environment variable `` is set (to any value). | @@ -23,17 +23,19 @@ The available predicate functions are: Predicates compose with **AND** semantics within a list: every entry must hold. `any(...)` gives OR within a single entry, `all(...)` gives an explicit AND group, and `not(...)` gives negation — together they form full boolean logic. They also compose with **AND** across levels (plugin ∧ group ∧ skill). -The argument of a leaf predicate (`crate`, `shell`, `path_exists`, `env`) is taken **verbatim** between the parentheses — it is *not* quoted. `shell(command -v rg)` runs `command -v rg`; do not wrap the argument in quotes (they would become part of the command). An inner `)` is fine as long as parentheses balance, so `shell(echo $(date))` works. The combinators `not`, `any`, and `all` take nested predicates as their arguments and may be nested arbitrarily, e.g. `not(any(env(CI), path_exists(.skip)))`. +The argument of a leaf predicate (`depends-on`, `shell`, `path_exists`, `env`) is taken **verbatim** between the parentheses — it is *not* quoted. `shell(command -v rg)` runs `command -v rg`; do not wrap the argument in quotes (they would become part of the command). An inner `)` is fine as long as parentheses balance, so `shell(echo $(date))` works. The combinators `not`, `any`, and `all` take nested predicates as their arguments and may be nested arbitrarily, e.g. `not(any(env(CI), path_exists(.skip)))`. + +> `crate(...)` is the retired spelling of `depends-on(...)` and is rejected at parse time with a migration hint. ## Crate-sourced skills and the witness -For a `[[skills]]` group with `source = "crate"`, the `crate(...)` predicates do double duty: they gate the group **and** name which crates' source trees to fetch skills from. The fetch set is the predicate's **witness** — the crates that participate in a *satisfying* evaluation: `crate(c)` contributes `c` when present, `any` contributes its true branches, `all` contributes all branches when it holds, and `not(...)` contributes nothing. So `all(crate(serde), env(USE_SERDE))` only fetches `serde` when `USE_SERDE` is set, while `any(crate(fd), crate(fdfind))` fetches whichever are present. +For a `[[skills]]` group with `source = "crate"`, the `depends-on(...)` predicates do double duty: they gate the group **and** name which crates' source trees to fetch skills from. The fetch set is the predicate's **witness** — the packages that participate in a *satisfying* evaluation: `depends-on(c)` contributes `c` when present, `any` contributes its true branches, `all` contributes all branches when it holds, and `not(...)` contributes nothing. So `all(depends-on(serde), env(USE_SERDE))` only fetches `serde` when `USE_SERDE` is set, while `any(depends-on(fd), depends-on(fdfind))` fetches whichever are present. -A group using `source = "crate"` must name at least one concrete crate in a **fetchable position** — somewhere (plugin- or group-level) that can appear in a witness. `crate(*)` alone is rejected since there is nothing concrete to fetch, and a crate named *only* under `not(...)` is rejected too: `not(crate(legacy))` gates the group but, having an empty witness, names no crate to fetch from. Put the crate positively (e.g. `crate(serde)`, or `any(crate(serde), not(crate(legacy)))`). +A group using `source = "crate"` must name at least one concrete dependency in a **fetchable position** — somewhere (plugin- or group-level) that can appear in a witness. `depends-on(*)` alone is rejected since there is nothing concrete to fetch, and a dependency named *only* under `not(...)` is rejected too: `not(depends-on(legacy))` gates the group but, having an empty witness, names no crate to fetch from. Put the dependency positively (e.g. `depends-on(serde)`, or `any(depends-on(serde), not(depends-on(legacy)))`). ## When predicates are evaluated -Predicates are evaluated at the same point the workspace's crate predicates are evaluated for that item: +Predicates are evaluated at the same point the workspace's dependency predicates are evaluated for that item: | Level | Evaluated | |-------|-----------| @@ -53,11 +55,11 @@ Hook-level predicates run at dispatch (not sync) so they observe live state — ```toml name = "my-plugin" -crates = ["*"] +depends-on = ["*"] predicates = ["path_exists(rg)", "shell(test -f Cargo.toml)"] [[skills]] -crates = ["serde"] +depends-on = ["serde"] predicates = ["path_exists(jq)"] source = "crate" @@ -77,13 +79,13 @@ predicates = ["path_exists(tool)"] ### Skill frontmatter (YAML) -Like `crates`, `predicates` is **comma-separated** on a single line in SKILL.md frontmatter. Commas inside `(...)` are not treated as separators, so a `shell(...)` command may itself contain commas: +Like `depends-on`, `predicates` is **comma-separated** on a single line in SKILL.md frontmatter. Commas inside `(...)` are not treated as separators, so a `shell(...)` command may itself contain commas: ```yaml --- name: my-skill description: Skill that depends on ripgrep -crates: serde +depends-on: serde predicates: path_exists(rg), shell(test -f Cargo.toml) --- ``` @@ -92,7 +94,7 @@ predicates: path_exists(rg), shell(test -f Cargo.toml) ```toml name = "uses-jq" -crates = ["*"] +depends-on = ["*"] predicates = ["path_exists(jq)"] [[hooks]] @@ -105,7 +107,7 @@ The hook here only registers if `jq` is on the user's `$PATH`. No error, no warn ## Combining predicates -`crate`, `env`, `not`, `any`, and `all` cover the cases plain `crates` lists can't: +`depends-on`, `env`, `not`, `any`, and `all` cover the cases plain `depends-on` lists can't: ```toml # Opt-in: only when a flag is set. @@ -117,17 +119,17 @@ predicates = ["not(path_exists(.skip-hooks))", "not(env(CI))"] # Tool packaged under different names across distros. predicates = ["any(path_exists(fd), path_exists(fdfind))"] -# A crate gate that also requires an env flag (vs. the bare `crates = ["serde"]`). -predicates = ["all(crate(serde), env(USE_SERDE))"] +# A dependency gate that also requires an env flag (vs. the bare `depends-on = ["serde"]`). +predicates = ["all(depends-on(serde), env(USE_SERDE))"] -# Apply only when a crate is absent (impossible with `crates`). -predicates = ["not(crate(legacy-thing))"] +# Apply only when a dependency is absent (impossible with `depends-on`). +predicates = ["not(depends-on(legacy-thing))"] ``` -These are equivalent — `crates` is just the terse form for the common case: +These are equivalent — `depends-on` is just the terse form for the common case: ```toml -crates = ["serde", "tokio"] +depends-on = ["serde", "tokio"] # is exactly -predicates = ["any(crate(serde), crate(tokio))"] +predicates = ["any(depends-on(serde), depends-on(tokio))"] ``` diff --git a/md/reference/skill-definition.md b/md/reference/skill-definition.md index 638a5a4c..33efb5bf 100644 --- a/md/reference/skill-definition.md +++ b/md/reference/skill-definition.md @@ -22,7 +22,7 @@ A `SKILL.md` file has YAML frontmatter followed by a markdown body: --- name: serde-basics description: Basic guidance for serde usage -crates: serde +depends-on: serde --- Prefer deriving `Serialize` and `Deserialize` on data types. @@ -34,7 +34,7 @@ Prefer deriving `Serialize` and `Deserialize` on data types. |-------|------|----------|-------------| | `name` | string | yes | Skill identifier. | | `description` | string | yes | Short description shown in skill listings. | -| `crates` | string | no | Comma-separated crate atoms this skill is about (e.g., `crates: serde, tokio>=1.0`). Narrows the enclosing `[[skills]]` group scope — cannot widen it. | +| `depends-on` | string | no | Comma-separated dependency atoms this skill is about (e.g., `depends-on: serde, tokio>=1.0`). Narrows the enclosing `[[skills]]` group scope — cannot widen it. | | `predicates` | string | no | Comma-separated predicates (`crate`, `shell`, `path_exists`, `env`, `not`, `any`, `all`); all must hold for the skill to activate. ANDed with plugin- and group-level predicates. See [Predicates](./predicates.md). | ## Crate atoms @@ -51,8 +51,8 @@ Crate atoms specify a crate name with an optional version constraint: - `serde=1.0` — compatible-with-1.0 (equivalent to `^1.0`) - `serde==1.0.219` — exact version -See [Crate predicates](./crate-predicates.md) for the full syntax. +See [Crate predicates](./depends-on.md) for the full syntax. ## Scope composition -`crates` can be declared at the `[[skills]]` group level (in the plugin TOML) and at the individual skill level (in SKILL.md frontmatter). They compose as AND: both layers must match for a skill to activate. A skill-level `crates` narrows the group's scope — it does not widen it. +`depends-on` can be declared at the `[[skills]]` group level (in the plugin TOML) and at the individual skill level (in SKILL.md frontmatter). They compose as AND: both layers must match for a skill to activate. A skill-level `depends-on` narrows the group's scope — it does not widen it. diff --git a/src/help_render.rs b/src/help_render.rs index 2dae80fa..2b5e99bf 100644 --- a/src/help_render.rs +++ b/src/help_render.rs @@ -193,12 +193,12 @@ mod tests { } fn crate_set(spec: &str) -> PredicateSet { - PredicateSet::from_crates(spec).unwrap() + PredicateSet::from_depends_on(spec).unwrap() } fn plugin_with( name: &str, - crates: &str, + depends_on: &str, subcommands: BTreeMap, ) -> ParsedPlugin { ParsedPlugin { @@ -206,7 +206,7 @@ mod tests { plugin: Plugin { name: name.into(), hooks: vec![], - predicates: crate_set(crates), + predicates: crate_set(depends_on), skills: vec![], mcp_servers: vec![], subcommands, @@ -218,12 +218,12 @@ mod tests { } } - fn subcommand(description: &str, audience: Audience, crates: Option<&str>) -> Subcommand { + fn subcommand(description: &str, audience: Audience, depends_on: Option<&str>) -> Subcommand { Subcommand { description: description.into(), audience, command: "ignored".into(), - predicates: crates.map(crate_set).unwrap_or_default(), + predicates: depends_on.map(crate_set).unwrap_or_default(), } } diff --git a/src/hook.rs b/src/hook.rs index be70dc72..93af9dc8 100644 --- a/src/hook.rs +++ b/src/hook.rs @@ -412,10 +412,7 @@ async fn prewarm_hook_sources(sym: &Symposium, deps: &mut WorkspaceDeps) { // Resolving the workspace runs cargo, so only do it when some hook's // gating references a concrete crate (mirrors dispatch). - let pairs = if plugins - .iter() - .any(|p| p.plugin.hooks_need_crate_resolution()) - { + let pairs = if plugins.iter().any(|p| p.plugin.hooks_need_dep_resolution()) { crate::crate_sources::crate_pairs(deps.crates()) } else { Vec::new() @@ -575,12 +572,9 @@ pub async fn dispatch_plugin_hooks( let plugins = crate::plugins::load_all_plugins(sym); // Resolving the workspace means running cargo, so only do it when some - // plugin's hook gating actually references a concrete crate (a `crate(*)` + // plugin's hook gating actually references a concrete crate (a `depends-on(*)` // wildcard or env/shell/path predicate never needs the crate graph). - let pairs = if plugins - .iter() - .any(|p| p.plugin.hooks_need_crate_resolution()) - { + let pairs = if plugins.iter().any(|p| p.plugin.hooks_need_dep_resolution()) { crate::crate_sources::crate_pairs(deps.crates()) } else { Vec::new() @@ -1037,8 +1031,8 @@ mod tests { .collect(), }, }; - // Plugin gate: `crate(*)` (always applies) plus the shell predicates. - let plugin_predicates = std::iter::once(crate::predicate::Predicate::CrateWildcard) + // Plugin gate: `depends-on(*)` (always applies) plus the shell predicates. + let plugin_predicates = std::iter::once(crate::predicate::Predicate::DependsOnWildcard) .chain( plugin_shell .into_iter() @@ -1112,13 +1106,13 @@ mod tests { /// A plugin gated on a concrete crate must not dispatch its hooks in a /// workspace that lacks the crate (regression: hooks used to fire for every - /// plugin regardless of its `crates`). + /// plugin regardless of its `depends-on`). #[test] fn dispatch_respects_plugin_crate_gate() { - // Replace the wildcard plugin gate with a concrete `crate(serde)`. + // Replace the wildcard plugin gate with a concrete `depends-on(serde)`. let mut plugin = plugin_with_hook(vec![], vec![]); plugin.plugin.predicates = crate::predicate::PredicateSet { - predicates: vec![crate::predicate::Predicate::Crate("serde".into(), None)], + predicates: vec![crate::predicate::Predicate::DependsOn("serde".into(), None)], }; // No serde in the workspace → the hook is skipped. diff --git a/src/plugins.rs b/src/plugins.rs index d7b3cc95..892caecb 100644 --- a/src/plugins.rs +++ b/src/plugins.rs @@ -16,7 +16,7 @@ pub type McpServerEntry = McpServer; /// An MCP server entry with optional activation predicates. /// -/// The server's `crates` and `predicates` fields are merged into one +/// The server's `depends-on` and `predicates` fields are merged into one /// [`PredicateSet`](crate::predicate::PredicateSet); the server is only /// registered when that set holds (ANDed with the plugin-level set). #[derive(Debug, Clone, Serialize)] @@ -32,8 +32,11 @@ pub struct PluginMcpServer { #[derive(Debug, Deserialize)] struct RawPluginMcpServer { + #[serde(default, rename = "depends-on")] + depends_on: Option, + /// Rejected: renamed to `depends-on`. #[serde(default)] - crates: Option, + crates: Option, #[serde(default)] predicates: crate::predicate::PredicateSet, #[serde(flatten)] @@ -41,14 +44,23 @@ struct RawPluginMcpServer { } impl RawPluginMcpServer { - fn validate(self) -> PluginMcpServer { - PluginMcpServer { - predicates: crate::predicate::PredicateSet::merged(self.crates, self.predicates), + fn validate(self) -> Result { + reject_crates_field(&self.crates)?; + Ok(PluginMcpServer { + predicates: crate::predicate::PredicateSet::merged(self.depends_on, self.predicates), server: self.server, - } + }) } } +/// Shared rejection for the retired `crates` field, with a migration hint. +fn reject_crates_field(crates: &Option) -> Result<()> { + if crates.is_some() { + bail!("the `crates` field has been renamed; use `depends-on` instead"); + } + Ok(()) +} + use symposium_install::UpdateLevel; /// Source declaration for a skill group. @@ -160,7 +172,7 @@ impl serde::Serialize for PluginSource { /// A `[[skills]]` entry from a plugin manifest. /// -/// The group's `crates` and `predicates` fields are merged into one +/// The group's `depends-on` and `predicates` fields are merged into one /// [`PredicateSet`](crate::predicate::PredicateSet) that gates the group and, /// for `source = "crate"`, locates the crate sources to fetch from. #[derive(Debug, Clone, Default, Serialize)] @@ -178,8 +190,11 @@ pub struct SkillGroup { #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] struct RawSkillGroup { + #[serde(default, rename = "depends-on")] + depends_on: Option, + /// Rejected: renamed to `depends-on`. #[serde(default)] - crates: Option, + crates: Option, #[serde(default)] predicates: crate::predicate::PredicateSet, #[serde(default)] @@ -188,8 +203,9 @@ struct RawSkillGroup { impl RawSkillGroup { fn validate(self) -> Result { + reject_crates_field(&self.crates)?; Ok(SkillGroup { - predicates: crate::predicate::PredicateSet::merged(self.crates, self.predicates), + predicates: crate::predicate::PredicateSet::merged(self.depends_on, self.predicates), source: self .source .map(RawPluginSource::validate) @@ -308,10 +324,10 @@ pub struct ParsedPlugin { #[derive(Debug, Clone, Serialize)] pub struct Plugin { pub name: String, - /// Activation predicates for this plugin — the plugin's `crates` (lowered to - /// `any(crate(...))`) merged with its `predicates`. Holds when every entry - /// holds. Evaluated at sync time (for skills/MCP), at subcommand lookup, and - /// at hook dispatch. + /// Activation predicates for this plugin — the plugin's `depends-on` + /// (lowered to `any(depends-on(...))`) merged with its `predicates`. Holds + /// when every entry holds. Evaluated at sync time (for skills/MCP), at + /// subcommand lookup, and at hook dispatch. pub predicates: crate::predicate::PredicateSet, /// Named installation entries available to hooks in this plugin. /// Order matches declaration order in the manifest. @@ -342,11 +358,11 @@ impl Plugin { /// True if gating this plugin's hooks (plugin-level plus hook-level /// predicates) needs the workspace crate graph — i.e. some predicate names a - /// concrete crate, not just `crate(*)`. Lets hook dispatch skip the cargo + /// concrete crate, not just `depends-on(*)`. Lets hook dispatch skip the cargo /// query when no crate is actually referenced. - pub fn hooks_need_crate_resolution(&self) -> bool { - self.predicates.has_concrete_crate() - || self.hooks.iter().any(|h| h.predicates.has_concrete_crate()) + pub fn hooks_need_dep_resolution(&self) -> bool { + self.predicates.has_concrete_dep() + || self.hooks.iter().any(|h| h.predicates.has_concrete_dep()) } /// Return MCP servers whose own predicates hold in `ctx`. @@ -386,7 +402,7 @@ pub struct Subcommand { pub description: String, pub audience: Audience, pub command: String, - /// Activation predicates for this subcommand (its `crates` lowered and + /// Activation predicates for this subcommand (its `depends-on` lowered and /// merged with its `predicates`). ANDed with the plugin-level set. #[serde( default, @@ -791,8 +807,11 @@ struct RawCustomPredicate { #[serde(deny_unknown_fields)] struct RawPluginManifest { name: String, + #[serde(default, rename = "depends-on")] + depends_on: crate::predicate::DependsOnList, + /// Rejected: renamed to `depends-on`. #[serde(default)] - crates: crate::predicate::CrateList, + crates: Option, #[serde(default)] predicates: crate::predicate::PredicateSet, #[serde(default)] @@ -840,8 +859,11 @@ struct RawSubcommand { /// Named installation (`"my-install"`) or inline installation table — /// same shape as `RawHook.command`. command: RawInstallationRef, + #[serde(default, rename = "depends-on")] + depends_on: Option, + /// Rejected: renamed to `depends-on`. #[serde(default)] - crates: Option, + crates: Option, #[serde(default)] predicates: crate::predicate::PredicateSet, } @@ -1415,7 +1437,7 @@ pub fn validate_source_dir(dir: &Path) -> Result> { /// Collect all crate names referenced in predicates across a plugin source directory. /// -/// Scans TOML plugin manifests (skill group `crates`) and +/// Scans TOML plugin manifests (skill group `depends-on`) and /// standalone SKILL.md files, returning deduplicated crate names. /// Items that fail to load are silently skipped. pub fn collect_crate_names_in_source_dir(dir: &Path) -> Result> { @@ -1426,18 +1448,18 @@ pub fn collect_crate_names_in_source_dir(dir: &Path) -> Result> { plugin_result .plugin .predicates - .collect_crate_names(&mut names); + .collect_dep_names(&mut names); for group in &plugin_result.plugin.skills { - group.predicates.collect_crate_names(&mut names); + group.predicates.collect_dep_names(&mut names); } for mcp in &plugin_result.plugin.mcp_servers { - mcp.predicates.collect_crate_names(&mut names); + mcp.predicates.collect_dep_names(&mut names); } } for skill_md in contents.skill_files { if let Ok(skill) = crate::skills::load_standalone_skill(&skill_md) { - skill.predicates.collect_crate_names(&mut names); + skill.predicates.collect_dep_names(&mut names); } } @@ -1559,8 +1581,9 @@ fn validate_manifest(manifest: RawPluginManifest) -> Result { )?); } + reject_crates_field(&manifest.crates)?; let predicates = - crate::predicate::PredicateSet::merged(Some(manifest.crates), manifest.predicates); + crate::predicate::PredicateSet::merged(Some(manifest.depends_on), manifest.predicates); let skills = manifest .skills .into_iter() @@ -1570,25 +1593,25 @@ fn validate_manifest(manifest: RawPluginManifest) -> Result { .mcp_servers .into_iter() .map(RawPluginMcpServer::validate) - .collect::>(); + .collect::>>()?; - // Every plugin must reference at least one crate (or custom predicate) - // somewhere — at the plugin, skill-group, hook, or MCP-server level — via - // `crates`, a `crate(...)` predicate, or a custom predicate. Otherwise it + // Every plugin must reference at least one dependency (or custom + // predicate) somewhere — at the plugin, skill-group, hook, or MCP-server + // level — via `depends-on` or a `depends-on(...)` predicate. Otherwise it // would never apply to any project. let has_custom_predicate = predicates .predicates .iter() .any(|p| matches!(p, crate::predicate::Predicate::Custom { .. })); - let mentions_crate = has_custom_predicate - || predicates.mentions_crate() - || skills.iter().any(|g| g.predicates.mentions_crate()) - || hooks.iter().any(|h| h.predicates.mentions_crate()) - || mcp_servers.iter().any(|m| m.predicates.mentions_crate()); - if !mentions_crate { + let mentions_dep = has_custom_predicate + || predicates.mentions_dep() + || skills.iter().any(|g| g.predicates.mentions_dep()) + || hooks.iter().any(|h| h.predicates.mentions_dep()) + || mcp_servers.iter().any(|m| m.predicates.mentions_dep()); + if !mentions_dep { bail!( - "plugin `{}` references no crate — add `crates = [...]` or a `crate(...)` predicate \ - at the plugin, `[[skills]]`, or `[[mcp_servers]]` level", + "plugin `{}` references no dependency — add `depends-on = [...]` or a \ + `depends-on(...)` predicate at the plugin, `[[skills]]`, or `[[mcp_servers]]` level", manifest.name ); } @@ -1653,9 +1676,11 @@ fn validate_subcommand( description, audience, command: raw_command, + depends_on, crates, predicates, } = raw; + reject_crates_field(&crates)?; if description.len() > MAX_SUBCOMMAND_DESCRIPTION_LEN { bail!("subcommand `{name}` description exceeds {MAX_SUBCOMMAND_DESCRIPTION_LEN} chars"); @@ -1673,7 +1698,7 @@ fn validate_subcommand( description, audience, command, - predicates: crate::predicate::PredicateSet::merged(crates, predicates), + predicates: crate::predicate::PredicateSet::merged(depends_on, predicates), }) } @@ -1750,26 +1775,26 @@ fn build_custom_predicate_registry( /// contributes a crate to fetch (its witness is always empty). /// /// Valid: -/// crates = ["serde"] + source = "crate" → fetch serde -/// crates = ["*"], group ["serde"] + source = "crate" → fetch serde -/// crates = ["*", "serde"] + source = "crate" → fetch serde -/// predicates = ["any(crate(a), crate(b))"] → fetch a and/or b +/// depends-on = ["serde"] + source = "crate" → fetch serde +/// depends-on = ["*"], group ["serde"] + source = "crate" → fetch serde +/// depends-on = ["*", "serde"] + source = "crate" → fetch serde +/// predicates = ["any(depends-on(a), depends-on(b))"] → fetch a and/or b /// /// Invalid: -/// crates = ["*"] + source = "crate" → no concrete crate -/// crates = ["*"], group ["*"] + source = "crate" → no concrete crate -/// predicates = ["not(crate(legacy))"] → no fetchable crate +/// depends-on = ["*"] + source = "crate" → no concrete crate +/// depends-on = ["*"], group ["*"] + source = "crate" → no concrete crate +/// predicates = ["not(depends-on(legacy))"] → no fetchable crate fn validate_skill_groups( plugin_predicates: &crate::predicate::PredicateSet, skills: &[SkillGroup], ) -> Result<()> { for (i, group) in skills.iter().enumerate() { if group.source == PluginSource::Crate { - let has_fetchable_crate = - plugin_predicates.has_fetchable_crate() || group.predicates.has_fetchable_crate(); - if !has_fetchable_crate { + let has_fetchable_dep = + plugin_predicates.has_fetchable_dep() || group.predicates.has_fetchable_dep(); + if !has_fetchable_dep { bail!( - "skills group {i} uses source = \"crate\" but no concrete `crate(...)` \ + "skills group {i} uses source = \"crate\" but no concrete `depends-on(...)` \ predicate is reachable in a fetchable position (plugin-level or \ group-level, not under `not(...)`) — at least one is required to \ resolve a crate to fetch skills from" @@ -1789,7 +1814,7 @@ mod tests { use crate::predicate::PredicateSet; fn pred_set(s: &str) -> PredicateSet { - PredicateSet::from_crates(s).unwrap() + PredicateSet::from_depends_on(s).unwrap() } fn ctx(crates: &[(String, semver::Version)]) -> crate::predicate::PredicateContext<'_> { @@ -1803,7 +1828,7 @@ mod tests { const SAMPLE: &str = indoc! {r#" name = "example-plugin" - crates = ["*"] + depends-on = ["*"] [[installations]] name = "tool" @@ -1828,17 +1853,17 @@ mod tests { fn parse_manifest_with_source_git_under_skills() { let toml = indoc! {r#" name = "remote-plugin" - crates = ["serde"] + depends-on = ["serde"] [[skills]] - crates = ["serde"] + depends-on = ["serde"] source.git = "https://github.com/org/repo/tree/main/serde" "#}; let plugin = from_str(toml).expect("parse"); assert_eq!(plugin.name, "remote-plugin"); assert_eq!(plugin.skills.len(), 1); let group = &plugin.skills[0]; - assert!(group.predicates.references_crate("serde")); + assert!(group.predicates.references_dep("serde")); assert!( matches!( &group.source, @@ -1853,20 +1878,20 @@ mod tests { fn parse_predicates_top_level() { let toml = indoc! {r#" name = "env-pred-plugin" - crates = ["*"] + depends-on = ["*"] predicates = ["shell(command -v rg)", "path_exists(Cargo.toml)"] [[skills]] - crates = ["serde"] + depends-on = ["serde"] "#}; let plugin = from_str(toml).expect("parse"); - // `crates = ["*"]` lowers to a leading `crate(*)`, then the two + // `depends-on = ["*"]` lowers to a leading `depends-on(*)`, then the two // function-call predicates. use crate::predicate::Predicate; assert_eq!( plugin.predicates.predicates, vec![ - Predicate::CrateWildcard, + Predicate::DependsOnWildcard, Predicate::Shell("command -v rg".into()), Predicate::PathExists("Cargo.toml".into()), ] @@ -1877,19 +1902,19 @@ mod tests { fn parse_predicates_on_skill_group() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[skills]] - crates = ["serde"] + depends-on = ["serde"] predicates = ["shell(command -v jq)"] "#}; let plugin = from_str(toml).expect("parse"); - // group `crates = ["serde"]` lowers to `crate(serde)`, plus the shell predicate. + // group `depends-on = ["serde"]` lowers to `depends-on(serde)`, plus the shell predicate. use crate::predicate::Predicate; assert_eq!( plugin.skills[0].predicates.predicates, vec![ - Predicate::Crate("serde".into(), None), + Predicate::DependsOn("serde".into(), None), Predicate::Shell("command -v jq".into()), ] ); @@ -1899,7 +1924,7 @@ mod tests { fn parse_predicates_on_hook() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[hooks]] name = "h" @@ -1913,28 +1938,77 @@ mod tests { #[test] fn predicates_default_empty() { - // With no `predicates`, the plugin gate is just the lowered `crates` - // (here `crate(*)`), and hooks default to no predicates. + // With no `predicates`, the plugin gate is just the lowered `depends-on` + // (here `depends-on(*)`), and hooks default to no predicates. let plugin = from_str(SAMPLE).expect("parse"); assert_eq!( plugin.predicates.predicates, - vec![crate::predicate::Predicate::CrateWildcard] + vec![crate::predicate::Predicate::DependsOnWildcard] ); assert!(plugin.hooks[0].predicates.is_empty()); } #[test] - fn parse_manifest_crates_as_array() { + fn parse_manifest_depends_on_as_array() { let toml = indoc! {r#" - name = "array-crates" - crates = ["*"] + name = "array-depends-on" + depends-on = ["*"] [[skills]] - crates = ["serde"] + depends-on = ["serde"] "#}; let plugin = from_str(toml).expect("parse"); let group = &plugin.skills[0]; - assert!(group.predicates.predicates[0].references_crate("serde")); + assert!(group.predicates.predicates[0].references_dep("serde")); + } + + #[test] + fn parse_manifest_rejects_renamed_crates_field() { + // Plugin level, group level, and MCP-server level all reject the old + // `crates` spelling with a migration hint. + for toml in [ + indoc! {r#" + name = "old-spelling" + crates = ["serde"] + "#}, + indoc! {r#" + name = "old-spelling" + depends-on = ["*"] + + [[skills]] + crates = ["serde"] + "#}, + indoc! {r#" + name = "old-spelling" + depends-on = ["*"] + + [[mcp_servers]] + name = "server" + command = "/usr/bin/true" + args = ["--stdio"] + env = [] + crates = ["serde"] + "#}, + ] { + let err = from_str(toml).unwrap_err(); + assert!( + err.to_string().contains("use `depends-on` instead"), + "expected migration hint, got: {err}" + ); + } + } + + #[test] + fn parse_manifest_rejects_renamed_crate_predicate() { + let toml = indoc! {r#" + name = "old-predicate" + predicates = ["crate(serde)"] + "#}; + let err = from_str(toml).unwrap_err(); + assert!( + err.to_string().contains("use `depends-on(serde)` instead"), + "expected migration hint, got: {err}" + ); } #[test] @@ -1945,7 +2019,7 @@ mod tests { "my-plugin/SYMPOSIUM.toml", indoc! {r#" name = "my-plugin" - crates = ["*"] + depends-on = ["*"] [[hooks]] name = "test" @@ -1959,7 +2033,7 @@ mod tests { --- name: assert-struct description: Check struct layout - crates: serde + depends-on: serde --- Use this skill. @@ -2002,7 +2076,7 @@ mod tests { indoc! {" --- name: root-skill - crates: serde + depends-on: serde --- Root level skill. @@ -2024,7 +2098,7 @@ mod tests { "SYMPOSIUM.toml", indoc! {r#" name = "root-plugin" - crates = ["*"] + depends-on = ["*"] "#}, )]); @@ -2044,7 +2118,7 @@ mod tests { "mixed/SYMPOSIUM.toml", indoc! {r#" name = "mixed-plugin" - crates = ["*"] + depends-on = ["*"] "#}, ), File( @@ -2052,7 +2126,7 @@ mod tests { indoc! {" --- name: ignored-skill - crates: serde + depends-on: serde --- This should be ignored. @@ -2075,7 +2149,7 @@ mod tests { "precedence-test/SYMPOSIUM.toml", indoc! {r#" name = "preferred-plugin" - crates = ["*"] + depends-on = ["*"] "#}, ), File( @@ -2101,7 +2175,7 @@ mod tests { "foo/SYMPOSIUM.toml", indoc! {r#" name = "foo-plugin" - crates = ["*"] + depends-on = ["*"] "#}, ), File( @@ -2109,7 +2183,7 @@ mod tests { indoc! {" --- name: foo-bar-skill - crates: serde + depends-on: serde --- Should be pruned. @@ -2120,7 +2194,7 @@ mod tests { indoc! {" --- name: baz-skill - crates: tokio + depends-on: tokio --- Should be found. @@ -2130,7 +2204,7 @@ mod tests { "baz/qux/SYMPOSIUM.toml", indoc! {r#" name = "qux-plugin" - crates = ["*"] + depends-on = ["*"] "#}, ), File( @@ -2138,7 +2212,7 @@ mod tests { indoc! {" --- name: qux-skill - crates: anyhow + depends-on: anyhow --- Should be pruned. @@ -2162,7 +2236,7 @@ mod tests { "good-plugin/SYMPOSIUM.toml", indoc! {r#" name = "good-plugin" - crates = ["serde"] + depends-on = ["serde"] "#}, ), File("bad-plugin/SYMPOSIUM.toml", "not valid toml {{{"), @@ -2172,7 +2246,7 @@ mod tests { --- name: my-skill description: A skill - crates: serde + depends-on: serde --- Body. @@ -2183,7 +2257,7 @@ mod tests { indoc! {" --- description: No name - crates: serde + depends-on: serde --- Body. @@ -2208,7 +2282,7 @@ mod tests { --- name: rust-best-practice description: [Critical] Best practice for Rust coding. - crates: serde + depends-on: serde --- Body. @@ -2231,10 +2305,10 @@ mod tests { "my-plugin/SYMPOSIUM.toml", indoc! {r#" name = "my-plugin" - crates = ["*"] + depends-on = ["*"] [[skills]] - crates = ["serde", "serde_json>=1.0"] + depends-on = ["serde", "serde_json>=1.0"] "#}, ), File( @@ -2243,7 +2317,7 @@ mod tests { --- name: my-skill description: A skill - crates: anyhow + depends-on: anyhow --- Body. @@ -2267,7 +2341,7 @@ mod tests { --- name: good description: Good skill - crates: serde + depends-on: serde --- Body. @@ -2300,7 +2374,7 @@ mod tests { fn path_at_wrong_level_is_rejected() { let toml = indoc! {r#" name = "Symposium" - crates = ["*"] + depends-on = ["*"] [[skills]] path = "." @@ -2316,19 +2390,19 @@ mod tests { fn parse_manifest_with_multiple_skill_groups() { let toml = indoc! {r#" name = "multi-group" - crates = ["*"] + depends-on = ["*"] [[skills]] - crates = ["serde"] + depends-on = ["serde"] [[skills]] - crates = ["tokio"] + depends-on = ["tokio"] "#}; let plugin = from_str(toml).expect("parse"); assert_eq!(plugin.name, "multi-group"); assert_eq!(plugin.skills.len(), 2); - assert!(plugin.skills[0].predicates.predicates[0].references_crate("serde")); - assert!(plugin.skills[1].predicates.predicates[0].references_crate("tokio")); + assert!(plugin.skills[0].predicates.predicates[0].references_dep("serde")); + assert!(plugin.skills[1].predicates.predicates[0].references_dep("tokio")); } #[test] @@ -2410,7 +2484,7 @@ mod tests { "good-plugin/SYMPOSIUM.toml", indoc! {r#" name = "good-plugin" - crates = ["serde"] + depends-on = ["serde"] [[hooks]] name = "some-hook" @@ -2510,7 +2584,7 @@ mod tests { fn cargo_install_used_as_hook() { let toml = indoc! {r#" name = "cargo-as-hook" - crates = ["*"] + depends-on = ["*"] [[installations]] name = "rg" @@ -2539,7 +2613,7 @@ mod tests { fn rtk_requirement_plus_github_command() { let toml = indoc! {r#" name = "rtk-plugin" - crates = ["*"] + depends-on = ["*"] [[installations]] name = "rtk" @@ -2572,7 +2646,7 @@ mod tests { fn github_script_on_installation_is_used() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[installations]] name = "g" @@ -2595,7 +2669,7 @@ mod tests { fn missing_named_installation_errors() { let toml = indoc! {r#" name = "bad-plugin" - crates = ["*"] + depends-on = ["*"] [[hooks]] name = "rewrite" @@ -2614,7 +2688,7 @@ mod tests { fn executable_and_script_together_errors() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[installations]] name = "x" @@ -2634,7 +2708,7 @@ mod tests { fn executable_set_on_both_layers_errors() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[installations]] name = "g" @@ -2662,7 +2736,7 @@ mod tests { fn executable_install_with_hook_script_errors() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[installations]] name = "g" @@ -2689,7 +2763,7 @@ mod tests { fn script_set_on_both_layers_errors() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[installations]] name = "g" @@ -2716,7 +2790,7 @@ mod tests { fn script_install_with_hook_executable_errors() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[installations]] name = "g" @@ -2743,7 +2817,7 @@ mod tests { fn hook_executable_and_script_together_errors() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[installations]] name = "setup" @@ -2770,7 +2844,7 @@ mod tests { fn hook_script_against_bare_installation_is_ok() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[installations]] name = "setup" @@ -2794,7 +2868,7 @@ mod tests { fn cargo_git_without_executable_errors() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[installations]] name = "tool" @@ -2819,7 +2893,7 @@ mod tests { fn args_set_on_both_layers_is_error() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[installations]] name = "rg" @@ -2846,7 +2920,7 @@ mod tests { fn hook_inherits_installation_args() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[installations]] name = "rg" @@ -2869,7 +2943,7 @@ mod tests { fn inline_installation_in_command() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[hooks]] name = "inline" @@ -2899,7 +2973,7 @@ mod tests { fn inline_no_source_executable() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[hooks]] name = "h" @@ -2922,7 +2996,7 @@ mod tests { fn inline_command_name_clash_errors() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[installations]] name = "h" @@ -2943,7 +3017,7 @@ mod tests { fn inline_command_with_hook_args_errors() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[hooks]] name = "h" @@ -2963,7 +3037,7 @@ mod tests { fn install_commands_field_is_carried_through() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[installations]] name = "rg" @@ -2990,7 +3064,7 @@ mod tests { fn install_commands_on_inline_command() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[hooks]] name = "h" @@ -3012,7 +3086,7 @@ mod tests { fn install_commands_on_inline_requirement() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[hooks]] name = "h" @@ -3038,7 +3112,7 @@ mod tests { fn hook_command_must_resolve_to_runnable() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[installations]] name = "setup" @@ -3059,7 +3133,7 @@ mod tests { fn cargo_without_executable_is_ok() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[installations]] name = "rg" @@ -3079,7 +3153,7 @@ mod tests { fn cargo_global_field_round_trips() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[installations]] name = "rg" @@ -3112,7 +3186,7 @@ mod tests { fn cargo_global_without_executable_errors() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[installations]] name = "rg" @@ -3138,7 +3212,7 @@ mod tests { fn cargo_git_field_round_trips() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[installations]] name = "tool" @@ -3172,7 +3246,7 @@ mod tests { fn inline_installation_can_have_requirements() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[installations]] name = "rtk" @@ -3197,7 +3271,7 @@ mod tests { fn pure_install_commands_installation_is_ok() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[installations]] name = "setup" @@ -3225,7 +3299,7 @@ mod tests { fn duplicate_installation_name_errors() { let toml = indoc! {r#" name = "dup" - crates = ["*"] + depends-on = ["*"] [[installations]] name = "x" @@ -3248,7 +3322,7 @@ mod tests { fn requirements_named_and_inline() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[installations]] name = "rtk" @@ -3283,7 +3357,7 @@ mod tests { fn installation_requirements_propagate_to_hook() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[installations]] name = "rtk" @@ -3313,7 +3387,7 @@ mod tests { fn installation_requirements_propagate_via_named_hook_requirement() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[installations]] name = "a" @@ -3342,7 +3416,7 @@ mod tests { fn installation_requirements_can_be_inline() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[installations]] name = "rtk-hooks" @@ -3375,7 +3449,7 @@ mod tests { fn installation_requirement_unknown_name_errors() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[installations]] name = "x" @@ -3394,7 +3468,7 @@ mod tests { fn requirements_unknown_named_errors() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[hooks]] name = "h" @@ -3415,7 +3489,7 @@ mod tests { fn parse_source_crate_shorthand() { let toml = indoc! {r#" name = "crate-shorthand" - crates = ["serde"] + depends-on = ["serde"] [[skills]] source = "crate" @@ -3428,7 +3502,7 @@ mod tests { fn parse_source_crate_path_is_error() { let toml = indoc! {r#" name = "bad" - crates = ["serde"] + depends-on = ["serde"] [[skills]] source.crate_path = "skills" @@ -3444,7 +3518,7 @@ mod tests { fn parse_source_crate_table_is_error() { let toml = indoc! {r#" name = "bad" - crates = ["serde"] + depends-on = ["serde"] [[skills]] source.crate = { name = "foo" } @@ -3460,7 +3534,7 @@ mod tests { fn parse_source_unknown_string_is_error() { let toml = indoc! {r#" name = "bad" - crates = ["serde"] + depends-on = ["serde"] [[skills]] source = "magic" @@ -3476,7 +3550,7 @@ mod tests { fn reject_path_and_git() { let toml = indoc! {r#" name = "bad" - crates = ["serde"] + depends-on = ["serde"] [[skills]] source.path = "." @@ -3492,7 +3566,7 @@ mod tests { fn crate_valid_with_plugin_non_wildcard() { let toml = indoc! {r#" name = "ok" - crates = ["serde"] + depends-on = ["serde"] [[skills]] source = "crate" @@ -3502,9 +3576,9 @@ mod tests { #[test] fn crate_reference_on_hook_satisfies_requirement() { - // A plugin whose only crate reference is a `crate(...)` predicate on a - // hook is valid — the hook is crate-gated even with no plugin-level - // `crates`. + // A plugin whose only dependency reference is a `depends-on(...)` + // predicate on a hook is valid — the hook is dependency-gated even + // with no plugin-level `depends-on`. let toml = indoc! {r#" name = "hook-crate" @@ -3512,20 +3586,20 @@ mod tests { name = "h" event = "PreToolUse" command = { script = "scripts/x.sh" } - predicates = ["crate(serde)"] + predicates = ["depends-on(serde)"] "#}; let plugin = from_str(toml).expect("should be valid"); - assert!(plugin.hooks[0].predicates.references_crate("serde")); + assert!(plugin.hooks[0].predicates.references_dep("serde")); } #[test] fn crate_valid_with_group_non_wildcard() { let toml = indoc! {r#" name = "ok" - crates = ["*"] + depends-on = ["*"] [[skills]] - crates = ["serde"] + depends-on = ["serde"] source = "crate" "#}; from_str(toml).expect("should be valid"); @@ -3535,7 +3609,7 @@ mod tests { fn crate_valid_with_mixed_wildcard_and_concrete() { let toml = indoc! {r#" name = "ok" - crates = ["*", "serde"] + depends-on = ["*", "serde"] [[skills]] source = "crate" @@ -3547,10 +3621,10 @@ mod tests { fn crate_reject_all_wildcards() { let toml = indoc! {r#" name = "bad" - crates = ["*"] + depends-on = ["*"] [[skills]] - crates = ["*"] + depends-on = ["*"] source = "crate" "#}; let err = from_str(toml).unwrap_err(); @@ -3561,7 +3635,7 @@ mod tests { fn crate_reject_wildcard_plugin_no_group_crates() { let toml = indoc! {r#" name = "bad" - crates = ["*"] + depends-on = ["*"] [[skills]] source = "crate" @@ -3580,7 +3654,7 @@ mod tests { [[skills]] source = "crate" - predicates = ["not(crate(legacy))"] + predicates = ["not(depends-on(legacy))"] "#}; let err = from_str(toml).unwrap_err(); assert!(err.to_string().contains("fetchable"), "{err}"); @@ -3595,7 +3669,7 @@ mod tests { [[skills]] source = "crate" - predicates = ["any(crate(serde), not(crate(legacy)))"] + predicates = ["any(depends-on(serde), not(depends-on(legacy)))"] "#}; from_str(toml).expect("should be valid"); } @@ -3611,7 +3685,7 @@ mod tests { fn roundtrip_source_crate() { let plugin = from_str(indoc! {r#" name = "rt" - crates = ["serde"] + depends-on = ["serde"] [[skills]] source = "crate" @@ -3625,7 +3699,7 @@ mod tests { fn roundtrip_source_path() { let plugin = from_str(indoc! {r#" name = "rt" - crates = ["serde"] + depends-on = ["serde"] [[skills]] source.path = "skills/v1" @@ -3646,7 +3720,7 @@ mod tests { fn roundtrip_source_git() { let plugin = from_str(indoc! {r#" name = "rt" - crates = ["serde"] + depends-on = ["serde"] [[skills]] source.git = "https://github.com/org/repo/tree/main/skills" @@ -3667,10 +3741,10 @@ mod tests { fn roundtrip_source_none() { let plugin = from_str(indoc! {r#" name = "rt" - crates = ["serde"] + depends-on = ["serde"] [[skills]] - crates = ["serde"] + depends-on = ["serde"] "#}) .unwrap(); let rt = roundtrip(&plugin); @@ -3685,7 +3759,7 @@ mod tests { fn serialize_crate_uses_string_form() { let plugin = from_str(indoc! {r#" name = "rt" - crates = ["serde"] + depends-on = ["serde"] [[skills]] source = "crate" @@ -3702,7 +3776,7 @@ mod tests { fn parse_subcommand_minimal_named() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[installations]] name = "tool" @@ -3725,7 +3799,7 @@ mod tests { fn parse_subcommand_audience_humans() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[installations]] name = "tool" @@ -3745,7 +3819,7 @@ mod tests { fn parse_subcommand_inline_command_is_promoted() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [subcommand.foo] description = "Run foo" @@ -3767,7 +3841,7 @@ mod tests { fn parse_subcommand_rejects_unknown_field() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [subcommand.foo] description = "Run foo" @@ -3783,7 +3857,7 @@ mod tests { fn parse_subcommand_rejects_reserved_name() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[installations]] name = "tool" @@ -3803,7 +3877,7 @@ mod tests { fn parse_subcommand_rejects_invalid_name_chars() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[installations]] name = "tool" @@ -3825,7 +3899,7 @@ mod tests { let toml = format!( r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[installations]] name = "tool" @@ -3846,7 +3920,7 @@ mod tests { fn parse_subcommand_unknown_command_reference_fails() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [subcommand.foo] description = "..." @@ -3864,7 +3938,7 @@ mod tests { "demo-plugin/SYMPOSIUM.toml", indoc! {r#" name = "demo-plugin" - crates = ["example-crate"] + depends-on = ["example-crate"] [[installations]] name = "example-tool" @@ -3902,7 +3976,7 @@ mod tests { fn parse_subcommand_with_crates_predicate() { let toml = indoc! {r#" name = "p" - crates = ["*"] + depends-on = ["*"] [[installations]] name = "tool" @@ -3912,11 +3986,11 @@ mod tests { [subcommand.foo] description = "Only for serde projects" command = "tool" - crates = ["serde"] + depends-on = ["serde"] "#}; let plugin = from_str(toml).expect("parse"); let sub = &plugin.subcommands["foo"]; - assert!(sub.predicates.references_crate("serde")); + assert!(sub.predicates.references_dep("serde")); } // --- custom predicate collision tests --- diff --git a/src/predicate.rs b/src/predicate.rs index 3420cda9..9743771f 100644 --- a/src/predicate.rs +++ b/src/predicate.rs @@ -4,12 +4,13 @@ //! dependency graph and the live environment. Two surface syntaxes lower to the //! same [`Predicate`] tree: //! -//! - The `crates` field uses **crate-atom** syntax (`serde`, `serde>=1.0`, `*`) -//! and lowers to `crate(...)` / `crate(*)` predicates, OR-combined into a -//! single `any(...)` that is appended to the same predicate list. +//! - The `depends-on` field uses **dependency-atom** syntax (`serde`, +//! `serde>=1.0`, `*`) and lowers to `depends-on(...)` / `depends-on(*)` +//! predicates, OR-combined into a single `any(...)` that is appended to the +//! same predicate list. //! - The `predicates` field uses **function-call** syntax: -//! - `crate()` — a workspace dependency is present (and its version -//! satisfies the optional requirement); `crate(*)` matches any workspace. +//! - `depends-on()` — a workspace dependency is present (and its version +//! satisfies the optional requirement); `depends-on(*)` matches any workspace. //! - `shell()` — `sh -c ` exits 0. //! - `path_exists()` — `` exists on disk, falling back to a `$PATH` //! lookup for bare names. @@ -21,47 +22,57 @@ //! Within a [`PredicateSet`] the entries are ANDed. //! //! Besides the boolean gate ([`PredicateSet::evaluate`]), predicates carry a -//! **witness**: the set of workspace crates that participate in a satisfying +//! **witness**: the set of workspace packages that participate in a satisfying //! evaluation. This drives `source = "crate"` skill resolution — see -//! [`PredicateSet::witness`] and [`union_matched_crates`]. +//! [`PredicateSet::witness`] and [`union_matched_packages`]. use std::path::Path; use std::process::Command; use anyhow::{Context, Result, bail}; -/// Names reserved for builtin predicates. Custom predicates must not use these. -pub const BUILTIN_PREDICATE_NAMES: &[&str] = - &["crate", "shell", "path_exists", "env", "not", "any", "all"]; +/// Names reserved for builtin predicates. Custom predicates must not use +/// these. `crate` is retired syntax but stays reserved so a custom predicate +/// can never squat on it. +pub const BUILTIN_PREDICATE_NAMES: &[&str] = &[ + "depends-on", + "crate", + "shell", + "path_exists", + "env", + "not", + "any", + "all", +]; /// The evaluation environment a predicate is checked against. /// -/// The crate graph is passed explicitly; the OS environment (`shell`, -/// `path_exists`, `env`) is read ambiently at evaluation time. Custom +/// The workspace dependency list is passed explicitly; the OS environment +/// (`shell`, `path_exists`, `env`) is read ambiently at evaluation time. Custom /// (plugin-defined) predicates are resolved entries whose results are cached /// for the lifetime of the context. #[derive(Debug)] pub struct PredicateContext<'a> { - pub crates: &'a [(String, semver::Version)], + pub deps: &'a [(String, semver::Version)], custom_entries: std::collections::HashMap, custom_cache: std::collections::HashMap<(String, String), CustomPredicateResult>, } impl<'a> PredicateContext<'a> { - pub fn new(crates: &'a [(String, semver::Version)]) -> Self { + pub fn new(deps: &'a [(String, semver::Version)]) -> Self { Self { - crates, + deps, custom_entries: std::collections::HashMap::new(), custom_cache: std::collections::HashMap::new(), } } pub fn with_custom_predicates( - crates: &'a [(String, semver::Version)], + deps: &'a [(String, semver::Version)], entries: std::collections::HashMap, ) -> Self { Self { - crates, + deps, custom_entries: entries, custom_cache: std::collections::HashMap::new(), } @@ -102,10 +113,10 @@ impl<'a> PredicateContext<'a> { /// A single predicate node. #[derive(Debug, Clone, PartialEq)] pub enum Predicate { - /// `crate()` / `crate()` — a workspace dep matches. - Crate(String, Option), - /// `crate(*)` / bare `*` — matches any workspace (even with zero deps). - CrateWildcard, + /// `depends-on()` / `depends-on()` — a workspace dep matches. + DependsOn(String, Option), + /// `depends-on(*)` / bare `*` — matches any workspace (even with zero deps). + DependsOnWildcard, /// `shell()` — passes when `sh -c ` exits 0. Shell(String), /// `path_exists()` — passes when `` exists (disk, then `$PATH`). @@ -131,10 +142,12 @@ impl Predicate { /// needed. pub fn evaluate(&self, ctx: &mut PredicateContext) -> bool { match self { - Predicate::Crate(name, version_req) => ctx.crates.iter().any(|(dep_name, dep_ver)| { - dep_name == name && version_req.as_ref().is_none_or(|req| req.matches(dep_ver)) - }), - Predicate::CrateWildcard => true, + Predicate::DependsOn(name, version_req) => { + ctx.deps.iter().any(|(dep_name, dep_ver)| { + dep_name == name && version_req.as_ref().is_none_or(|req| req.matches(dep_ver)) + }) + } + Predicate::DependsOnWildcard => true, Predicate::Shell(cmd) => run_shell(cmd), Predicate::PathExists(arg) => path_exists(arg), Predicate::Env(name, expected) => env_matches(name, expected.as_deref()), @@ -147,16 +160,17 @@ impl Predicate { /// Evaluate, returning `None` when false and `Some(witness)` when true. /// - /// The witness is the set of workspace crates that participate in the - /// satisfying evaluation: `crate(c)` contributes `c` when present, `any` - /// unions the witnesses of its *true* children, `all` unions all children's - /// witnesses (when all hold), and `not` contributes nothing (negation is - /// about absence). Non-crate leaves contribute an empty witness. + /// The witness is the set of workspace packages that participate in the + /// satisfying evaluation: `depends-on(d)` contributes `d` when present, + /// `any` unions the witnesses of its *true* children, `all` unions all + /// children's witnesses (when all hold), and `not` contributes nothing + /// (negation is about absence). Non-dependency leaves contribute an empty + /// witness. pub fn witness(&self, ctx: &mut PredicateContext) -> Option> { match self { - Predicate::Crate(name, version_req) => { + Predicate::DependsOn(name, version_req) => { let hits: Vec<_> = ctx - .crates + .deps .iter() .filter(|(dep_name, dep_ver)| { dep_name == name @@ -166,7 +180,7 @@ impl Predicate { .collect(); if hits.is_empty() { None } else { Some(hits) } } - Predicate::CrateWildcard => Some(Vec::new()), + Predicate::DependsOnWildcard => Some(Vec::new()), Predicate::Shell(cmd) => run_shell(cmd).then(Vec::new), Predicate::PathExists(arg) => path_exists(arg).then(Vec::new), Predicate::Env(name, expected) => env_matches(name, expected.as_deref()).then(Vec::new), @@ -203,71 +217,74 @@ impl Predicate { } } - /// Returns true if this predicate references the given crate name anywhere - /// (including inside combinators and negations). - pub fn references_crate(&self, name: &str) -> bool { + /// Returns true if this predicate references the given dependency name + /// anywhere (including inside combinators and negations). + pub fn references_dep(&self, name: &str) -> bool { match self { - Predicate::Crate(n, _) => n == name, - Predicate::Not(p) => p.references_crate(name), - Predicate::Any(v) | Predicate::All(v) => v.iter().any(|p| p.references_crate(name)), + Predicate::DependsOn(n, _) => n == name, + Predicate::Not(p) => p.references_dep(name), + Predicate::Any(v) | Predicate::All(v) => v.iter().any(|p| p.references_dep(name)), Predicate::Custom { .. } => false, _ => false, } } - /// True if this predicate mentions any crate (concrete or `crate(*)`). - pub fn mentions_crate(&self) -> bool { + /// True if this predicate mentions any dependency (concrete or + /// `depends-on(*)`). + pub fn mentions_dep(&self) -> bool { match self { - Predicate::Crate(..) | Predicate::CrateWildcard => true, - Predicate::Not(p) => p.mentions_crate(), - Predicate::Any(v) | Predicate::All(v) => v.iter().any(Predicate::mentions_crate), + Predicate::DependsOn(..) | Predicate::DependsOnWildcard => true, + Predicate::Not(p) => p.mentions_dep(), + Predicate::Any(v) | Predicate::All(v) => v.iter().any(Predicate::mentions_dep), Predicate::Custom { .. } => false, _ => false, } } - /// True if this predicate names a *concrete* crate (`crate(serde)`), as - /// opposed to only `crate(*)`. Non-allocating — used on the hook hot path. - pub fn has_concrete_crate(&self) -> bool { + /// True if this predicate names a *concrete* dependency + /// (`depends-on(serde)`), as opposed to only `depends-on(*)`. + /// Non-allocating — used on the hook hot path. + pub fn has_concrete_dep(&self) -> bool { match self { - Predicate::Crate(..) => true, - Predicate::Not(p) => p.has_concrete_crate(), - Predicate::Any(v) | Predicate::All(v) => v.iter().any(Predicate::has_concrete_crate), + Predicate::DependsOn(..) => true, + Predicate::Not(p) => p.has_concrete_dep(), + Predicate::Any(v) | Predicate::All(v) => v.iter().any(Predicate::has_concrete_dep), Predicate::Custom { .. } => false, _ => false, } } - /// True if this predicate names a concrete crate in a position that can - /// appear in a [`witness`](Self::witness) — i.e. a `crate(serde)` not under - /// any `not(...)`. A crate beneath a negation never contributes a crate to - /// fetch from (the `Not` arm of `witness` discards its inner witness), so - /// it cannot anchor a `source = "crate"` group. Custom predicates may - /// produce witnesses at runtime, so they count as fetchable. - pub fn has_fetchable_crate(&self) -> bool { + /// True if this predicate names a concrete dependency in a position that + /// can appear in a [`witness`](Self::witness) — i.e. a `depends-on(serde)` + /// not under any `not(...)`. A dependency beneath a negation never + /// contributes a package to fetch from (the `Not` arm of `witness` + /// discards its inner witness), so it cannot anchor a `source = "crate"` + /// group. Custom predicates may produce witnesses at runtime, so they + /// count as fetchable. + pub fn has_fetchable_dep(&self) -> bool { match self { - Predicate::Crate(..) => true, + Predicate::DependsOn(..) => true, Predicate::Custom { .. } => true, Predicate::Not(_) => false, - Predicate::Any(v) | Predicate::All(v) => v.iter().any(Predicate::has_fetchable_crate), + Predicate::Any(v) | Predicate::All(v) => v.iter().any(Predicate::has_fetchable_dep), _ => false, } } - /// Collect every crate name referenced anywhere in this predicate. + /// Collect every dependency name referenced anywhere in this predicate. /// /// Used for crates.io existence validation, so it ignores tree position - /// (a crate named under `not(...)` is still validated). Custom predicates - /// are a no-op — their crate names are dynamic. - pub fn collect_crate_names(&self, out: &mut std::collections::BTreeSet) { + /// (a dependency named under `not(...)` is still validated). Custom + /// predicates are a no-op — their names are dynamic. + pub fn collect_dep_names(&self, out: &mut std::collections::BTreeSet) { match self { - Predicate::Crate(name, _) => { + Predicate::DependsOn(name, _) => { out.insert(name.clone()); } - Predicate::Not(p) => p.collect_crate_names(out), + Predicate::Not(p) => p.collect_dep_names(out), Predicate::Any(v) | Predicate::All(v) => { for p in v { - p.collect_crate_names(out); + p.collect_dep_names(out); } } Predicate::Custom { .. } => {} @@ -290,23 +307,23 @@ impl PredicateSet { }) } - /// Build a set from **crate-atom** syntax (the `crates` field), lowering the - /// OR-combined atoms into a single `any(...)` predicate. Empty input yields - /// an empty set. - pub fn from_crates(input: &str) -> Result { + /// Build a set from **dependency-atom** syntax (the `depends-on` field), + /// lowering the OR-combined atoms into a single `any(...)` predicate. + /// Empty input yields an empty set. + pub fn from_depends_on(input: &str) -> Result { Ok(Self { - predicates: CrateList::parse(input)? + predicates: DependsOnList::parse(input)? .into_predicate() .into_iter() .collect(), }) } - /// Combine a lowered `crates` field with a `predicates` field into one set. - /// The `crates` atoms become a single leading `any(...)` predicate. - pub fn merged(crates: Option, predicates: PredicateSet) -> PredicateSet { + /// Combine a lowered `depends-on` field with a `predicates` field into one + /// set. The `depends-on` atoms become a single leading `any(...)` predicate. + pub fn merged(depends_on: Option, predicates: PredicateSet) -> PredicateSet { let mut list = Vec::new(); - if let Some(p) = crates.and_then(CrateList::into_predicate) { + if let Some(p) = depends_on.and_then(DependsOnList::into_predicate) { list.push(p); } list.extend(predicates.predicates); @@ -321,51 +338,52 @@ impl PredicateSet { /// Witness for the whole set (treated as one big `all(...)`): `None` if any /// predicate is false, otherwise the deduplicated union of witnesses. pub fn witness(&self, ctx: &mut PredicateContext) -> Option> { - let mut crates = Vec::new(); + let mut packages = Vec::new(); for p in &self.predicates { - crates.extend(p.witness(ctx)?); + packages.extend(p.witness(ctx)?); } - Some(dedup_crates(crates)) + Some(dedup_packages(packages)) } pub fn is_empty(&self) -> bool { self.predicates.is_empty() } - pub fn collect_crate_names(&self, out: &mut std::collections::BTreeSet) { + pub fn collect_dep_names(&self, out: &mut std::collections::BTreeSet) { for p in &self.predicates { - p.collect_crate_names(out); + p.collect_dep_names(out); } } - /// True if any `crate(...)` predicate (non-wildcard) appears anywhere. - pub fn has_concrete_crate(&self) -> bool { - self.predicates.iter().any(Predicate::has_concrete_crate) + /// True if any `depends-on(...)` predicate (non-wildcard) appears anywhere. + pub fn has_concrete_dep(&self) -> bool { + self.predicates.iter().any(Predicate::has_concrete_dep) } - /// True if a concrete crate appears in a fetchable (non-negated) position. - /// Gates `source = "crate"` validation: such a group must name at least one - /// crate it can actually fetch skills from. - pub fn has_fetchable_crate(&self) -> bool { - self.predicates.iter().any(Predicate::has_fetchable_crate) + /// True if a concrete dependency appears in a fetchable (non-negated) + /// position. Gates `source = "crate"` validation: such a group must name + /// at least one dependency it can actually fetch skills from. + pub fn has_fetchable_dep(&self) -> bool { + self.predicates.iter().any(Predicate::has_fetchable_dep) } - /// True if any crate predicate (including `crate(*)`) appears anywhere. - pub fn mentions_crate(&self) -> bool { - self.predicates.iter().any(Predicate::mentions_crate) + /// True if any dependency predicate (including `depends-on(*)`) appears + /// anywhere. + pub fn mentions_dep(&self) -> bool { + self.predicates.iter().any(Predicate::mentions_dep) } - /// True if any predicate references the given crate name. - pub fn references_crate(&self, name: &str) -> bool { - self.predicates.iter().any(|p| p.references_crate(name)) + /// True if any predicate references the given dependency name. + pub fn references_dep(&self, name: &str) -> bool { + self.predicates.iter().any(|p| p.references_dep(name)) } } -/// Union the witnesses of several predicate sets, deduplicated by crate name. +/// Union the witnesses of several predicate sets, deduplicated by name. /// /// A set whose gate is false contributes nothing. Drives `source = "crate"` -/// resolution: the concrete crates whose source trees to fetch skills from. -pub fn union_matched_crates( +/// resolution: the concrete packages whose source trees to fetch skills from. +pub fn union_matched_packages( sets: &[&PredicateSet], ctx: &mut PredicateContext, ) -> Vec<(String, semver::Version)> { @@ -383,30 +401,30 @@ pub fn union_matched_crates( result } -fn dedup_crates(crates: Vec<(String, semver::Version)>) -> Vec<(String, semver::Version)> { +fn dedup_packages(packages: Vec<(String, semver::Version)>) -> Vec<(String, semver::Version)> { let mut seen = std::collections::HashSet::new(); - crates + packages .into_iter() .filter(|(name, _)| seen.insert(name.clone())) .collect() } -// --- the `crates` field: a list of crate atoms, OR-combined --- +// --- the `depends-on` field: a list of dependency atoms, OR-combined --- -/// The parsed `crates = [...]` field — a list of crate atoms. Lowers to a +/// The parsed `depends-on = [...]` field — a list of crate atoms. Lowers to a /// single `any(...)` predicate appended to the enclosing predicate list. #[derive(Debug, Clone, Default, PartialEq)] -pub struct CrateList(pub Vec); +pub struct DependsOnList(pub Vec); #[derive(Debug, serde::Deserialize)] #[serde(untagged)] -enum RawCrateList { +enum RawDependsOnList { One(String), Many(Vec), } -impl CrateList { - /// Parse comma-separated crate atoms (`serde, tokio>=1.0, *`). +impl DependsOnList { + /// Parse comma-separated dependency atoms (`serde, tokio>=1.0, *`). /// /// Commas inside balanced parentheses are preserved so that custom /// predicates like `battery_pack(a, b)` are not split incorrectly. @@ -416,8 +434,8 @@ impl CrateList { .map(|s| s.trim()) .filter(|s| !s.is_empty()) .map(|s| { - parse_crate_atom(s) - .with_context(|| format!("failed to parse crate predicate: {s:?}")) + parse_dep_atom(s) + .with_context(|| format!("failed to parse depends-on predicate: {s:?}")) }) .collect::>>()?; Ok(Self(atoms)) @@ -434,17 +452,17 @@ impl CrateList { } } -impl<'de> serde::Deserialize<'de> for CrateList { +impl<'de> serde::Deserialize<'de> for DependsOnList { fn deserialize>(deserializer: D) -> Result { - // Accept either a single string (`crates = "serde"`) or a sequence - // (`crates = ["serde", "tokio>=1.0"]`). - let atoms = match RawCrateList::deserialize(deserializer)? { - RawCrateList::One(s) => vec![s], - RawCrateList::Many(v) => v, + // Accept either a single string (`depends-on = "serde"`) or a sequence + // (`depends-on = ["serde", "tokio>=1.0"]`). + let atoms = match RawDependsOnList::deserialize(deserializer)? { + RawDependsOnList::One(s) => vec![s], + RawDependsOnList::Many(v) => v, }; let predicates = atoms .iter() - .map(|s| parse_crate_atom(s.trim())) + .map(|s| parse_dep_atom(s.trim())) .collect::>>() .map_err(serde::de::Error::custom)?; Ok(Self(predicates)) @@ -493,7 +511,8 @@ fn parse(input: &str) -> Result { let arg = trimmed[open + 1..trimmed.len() - 1].trim(); match name { - "crate" => parse_crate_atom(arg), + "depends-on" => parse_dep_atom(arg), + "crate" => bail!("`crate({arg})` is no longer supported; use `depends-on({arg})` instead"), "shell" => Ok(Predicate::Shell(arg.to_string())), "path_exists" => Ok(Predicate::PathExists(arg.to_string())), "env" => parse_env(arg), @@ -574,16 +593,17 @@ fn split_top_level(input: &str) -> Vec { out } -// --- crate-atom parsing (`serde`, `serde>=1.0`, `*`) --- +// --- dependency-atom parsing (`serde`, `serde>=1.0`, `*`) --- -/// Parse a single crate atom into a `Crate` / `CrateWildcard` predicate. -pub fn parse_crate_atom(input: &str) -> Result { +/// Parse a single dependency atom into a `DependsOn` / `DependsOnWildcard` +/// predicate. +pub fn parse_dep_atom(input: &str) -> Result { let input = input.trim(); if input.is_empty() { - bail!("empty crate predicate"); + bail!("empty depends-on predicate"); } if input == "*" { - return Ok(Predicate::CrateWildcard); + return Ok(Predicate::DependsOnWildcard); } let mut parser = AtomParser::new(input); let pred = parser.parse_atom()?; @@ -622,7 +642,7 @@ impl<'a> AtomParser<'a> { self.skip_whitespace(); let start = self.pos; - // Consume crate name: [a-zA-Z0-9_-]+ + // Consume dependency name: [a-zA-Z0-9_-]+ while self.pos < self.input.len() { let c = self.input.as_bytes()[self.pos]; if c.is_ascii_alphanumeric() || c == b'_' || c == b'-' { @@ -635,19 +655,19 @@ impl<'a> AtomParser<'a> { let name = &self.input[start..self.pos]; if name.is_empty() { bail!( - "expected crate name at position {}: {:?}", + "expected dependency name at position {}: {:?}", start, self.remaining() ); } - // Function-call syntax is NOT valid in crate-atom position. The - // `crates` field accepts only bare names + optional version constraints. - // Full predicate expressions (including custom predicates) belong in the - // `predicates` field. + // Function-call syntax is NOT valid in dependency-atom position. The + // `depends-on` field accepts only bare names + optional version + // constraints. Full predicate expressions (including custom + // predicates) belong in the `predicates` field. if self.pos < self.input.len() && self.input.as_bytes()[self.pos] == b'(' { bail!( - "function-call syntax `{name}(...)` is not valid in the `crates` field; \ + "function-call syntax `{name}(...)` is not valid in the `depends-on` field; \ use the `predicates` field instead" ); } @@ -681,7 +701,7 @@ impl<'a> AtomParser<'a> { None }; - Ok(Predicate::Crate(name.to_string(), version_req)) + Ok(Predicate::DependsOn(name.to_string(), version_req)) } } @@ -763,9 +783,9 @@ impl<'de> serde::Deserialize<'de> for PredicateSet { impl std::fmt::Display for Predicate { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Predicate::Crate(name, None) => write!(f, "crate({name})"), - Predicate::Crate(name, Some(req)) => write!(f, "crate({name}{req})"), - Predicate::CrateWildcard => write!(f, "crate(*)"), + Predicate::DependsOn(name, None) => write!(f, "depends-on({name})"), + Predicate::DependsOn(name, Some(req)) => write!(f, "depends-on({name}{req})"), + Predicate::DependsOnWildcard => write!(f, "depends-on(*)"), Predicate::Shell(cmd) => write!(f, "shell({cmd})"), Predicate::PathExists(arg) => write!(f, "path_exists({arg})"), Predicate::Env(name, None) => write!(f, "env({name})"), @@ -976,40 +996,42 @@ mod tests { #[test] fn parse_crate_atom_bare_and_versioned() { assert_eq!( - parse_crate_atom("serde").unwrap(), - Predicate::Crate("serde".into(), None) + parse_dep_atom("serde").unwrap(), + Predicate::DependsOn("serde".into(), None) ); assert_eq!( - parse_crate_atom("serde>=1.0").unwrap(), - Predicate::Crate( + parse_dep_atom("serde>=1.0").unwrap(), + Predicate::DependsOn( "serde".into(), Some(semver::VersionReq::parse(">=1.0").unwrap()) ) ); - assert_eq!(parse_crate_atom("*").unwrap(), Predicate::CrateWildcard); + assert_eq!(parse_dep_atom("*").unwrap(), Predicate::DependsOnWildcard); } #[test] fn crate_list_lowers_to_any() { - assert_eq!(CrateList::parse("").unwrap().into_predicate(), None); + assert_eq!(DependsOnList::parse("").unwrap().into_predicate(), None); assert_eq!( - CrateList::parse("serde").unwrap().into_predicate(), - Some(Predicate::Crate("serde".into(), None)) + DependsOnList::parse("serde").unwrap().into_predicate(), + Some(Predicate::DependsOn("serde".into(), None)) ); assert_eq!( - CrateList::parse("serde, tokio").unwrap().into_predicate(), + DependsOnList::parse("serde, tokio") + .unwrap() + .into_predicate(), Some(Predicate::Any(vec![ - Predicate::Crate("serde".into(), None), - Predicate::Crate("tokio".into(), None), + Predicate::DependsOn("serde".into(), None), + Predicate::DependsOn("tokio".into(), None), ])) ); - // Function-call syntax is rejected in the `crates` field. - assert!(CrateList::parse("bp(cli, web)").is_err()); - assert!(CrateList::parse("serde, bp(a, b)").is_err()); - assert!(CrateList::parse("all()").is_err()); - assert!(CrateList::parse("crate(serde)").is_err()); - assert!(CrateList::parse("not(serde)").is_err()); - assert!(CrateList::parse("shell(true)").is_err()); + // Function-call syntax is rejected in the `depends-on` field. + assert!(DependsOnList::parse("bp(cli, web)").is_err()); + assert!(DependsOnList::parse("serde, bp(a, b)").is_err()); + assert!(DependsOnList::parse("all()").is_err()); + assert!(DependsOnList::parse("depends-on(serde)").is_err()); + assert!(DependsOnList::parse("not(serde)").is_err()); + assert!(DependsOnList::parse("shell(true)").is_err()); } // --- function-call parsing --- @@ -1022,26 +1044,38 @@ mod tests { assert!(parse("*").is_err()); } + #[test] + fn parse_rejects_renamed_crate_predicate() { + let err = parse("crate(serde)").unwrap_err(); + assert!( + err.to_string().contains("use `depends-on(serde)` instead"), + "expected migration hint, got: {err}" + ); + } + #[test] fn parse_function_calls() { assert_eq!( - parse("crate(serde)").unwrap(), - Predicate::Crate("serde".into(), None) + parse("depends-on(serde)").unwrap(), + Predicate::DependsOn("serde".into(), None) + ); + assert_eq!( + parse("depends-on(*)").unwrap(), + Predicate::DependsOnWildcard ); - assert_eq!(parse("crate(*)").unwrap(), Predicate::CrateWildcard); assert_eq!( parse("shell(command -v rg)").unwrap(), Predicate::Shell("command -v rg".into()) ); assert_eq!(parse("env(CI)").unwrap(), Predicate::Env("CI".into(), None)); assert_eq!( - parse("not(crate(serde))").unwrap(), - Predicate::Not(Box::new(Predicate::Crate("serde".into(), None))) + parse("not(depends-on(serde))").unwrap(), + Predicate::Not(Box::new(Predicate::DependsOn("serde".into(), None))) ); assert_eq!( - parse("any(crate(a), path_exists(rg))").unwrap(), + parse("any(depends-on(a), path_exists(rg))").unwrap(), Predicate::Any(vec![ - Predicate::Crate("a".into(), None), + Predicate::DependsOn("a".into(), None), Predicate::PathExists("rg".into()), ]) ); @@ -1061,22 +1095,26 @@ mod tests { #[test] fn evaluate_crate_and_wildcard() { let w = ws(&[("serde", "1.0.0")]); - assert!(parse("crate(serde)").unwrap().evaluate(&mut ctx(&w))); - assert!(!parse("crate(tokio)").unwrap().evaluate(&mut ctx(&w))); - assert!(parse("crate(*)").unwrap().evaluate(&mut ctx(&[]))); + assert!(parse("depends-on(serde)").unwrap().evaluate(&mut ctx(&w))); + assert!(!parse("depends-on(tokio)").unwrap().evaluate(&mut ctx(&w))); + assert!(parse("depends-on(*)").unwrap().evaluate(&mut ctx(&[]))); } #[test] fn evaluate_combinators() { let w = ws(&[("serde", "1.0.0")]); - assert!(parse("not(crate(tokio))").unwrap().evaluate(&mut ctx(&w))); assert!( - parse("any(crate(tokio), crate(serde))") + parse("not(depends-on(tokio))") .unwrap() .evaluate(&mut ctx(&w)) ); assert!( - !parse("all(crate(serde), crate(tokio))") + parse("any(depends-on(tokio), depends-on(serde))") + .unwrap() + .evaluate(&mut ctx(&w)) + ); + assert!( + !parse("all(depends-on(serde), depends-on(tokio))") .unwrap() .evaluate(&mut ctx(&w)) ); @@ -1088,14 +1126,14 @@ mod tests { // `witness(...).is_some()` for every shape. let w = ws(&[("serde", "1.0.0")]); for input in [ - "crate(serde)", - "crate(tokio)", - "crate(*)", - "not(crate(tokio))", - "any(crate(tokio), shell(true))", - "all(crate(serde), env(PATH))", - "all(crate(serde), crate(tokio))", - "not(any(crate(serde), env(PATH)))", + "depends-on(serde)", + "depends-on(tokio)", + "depends-on(*)", + "not(depends-on(tokio))", + "any(depends-on(tokio), shell(true))", + "all(depends-on(serde), env(PATH))", + "all(depends-on(serde), depends-on(tokio))", + "not(any(depends-on(serde), env(PATH)))", ] { let p = parse(input).unwrap(); assert_eq!( @@ -1116,8 +1154,9 @@ mod tests { #[test] fn witness_example_one_all_gates_crate2() { - // any(crate(c1), all(crate(c2), env(USE_C2))) - let p = parse("any(crate(c1), all(crate(c2), env(SYMPOSIUM_TEST_UNSET_XYZ)))").unwrap(); + // any(depends-on(c1), all(depends-on(c2), env(USE_C2))) + let p = parse("any(depends-on(c1), all(depends-on(c2), env(SYMPOSIUM_TEST_UNSET_XYZ)))") + .unwrap(); let w = ws(&[("c1", "1.0.0"), ("c2", "1.0.0")]); // env unset -> all(...) is a dead branch -> only c1 let names: Vec<_> = p @@ -1131,10 +1170,10 @@ mod tests { #[test] fn witness_example_three_not_excludes_crate2() { - // any(crate(c1), all(not(env(SKIP)), crate(c2))) with SKIP "set" + // any(depends-on(c1), all(not(env(SKIP)), depends-on(c2))) with SKIP "set" // Model "SKIP set" by asserting against an env-equality we force true via // a value compare on an unset var is false; instead use a present var. - let p = parse("any(crate(c1), all(not(env(PATH)), crate(c2)))").unwrap(); + let p = parse("any(depends-on(c1), all(not(env(PATH)), depends-on(c2)))").unwrap(); let w = ws(&[("c1", "1.0.0"), ("c2", "1.0.0")]); // PATH is set -> not(env(PATH)) false -> all dead -> only c1 let names: Vec<_> = p @@ -1148,9 +1187,9 @@ mod tests { #[test] fn witness_unions_all_true_branches() { - // any(crate(c1), any(env(PATH), crate(c2))) — both c1 and c2 present and - // their crate(...) branches are independently true. - let p = parse("any(crate(c1), any(env(PATH), crate(c2)))").unwrap(); + // any(depends-on(c1), any(env(PATH), depends-on(c2))) — both c1 and c2 present and + // their depends-on(...) branches are independently true. + let p = parse("any(depends-on(c1), any(env(PATH), depends-on(c2)))").unwrap(); let w = ws(&[("c1", "1.0.0"), ("c2", "1.0.0")]); let mut names: Vec<_> = p .witness(&mut ctx(&w)) @@ -1164,16 +1203,16 @@ mod tests { #[test] fn witness_false_gate_is_none() { - let p = parse("crate(absent)").unwrap(); + let p = parse("depends-on(absent)").unwrap(); assert!(p.witness(&mut ctx(&[])).is_none()); } #[test] fn union_matched_crates_dedups_across_sets() { - let plugin = PredicateSet::from_crates("serde").unwrap(); - let group = PredicateSet::from_crates("serde, tokio").unwrap(); + let plugin = PredicateSet::from_depends_on("serde").unwrap(); + let group = PredicateSet::from_depends_on("serde, tokio").unwrap(); let w = ws(&[("serde", "1.0.0"), ("tokio", "1.0.0")]); - let result = union_matched_crates(&[&plugin, &group], &mut ctx(&w)); + let result = union_matched_packages(&[&plugin, &group], &mut ctx(&w)); let mut names: Vec<_> = result.into_iter().map(|(n, _)| n).collect(); names.sort(); assert_eq!(names, vec!["serde", "tokio"]); @@ -1183,56 +1222,60 @@ mod tests { #[test] fn collect_and_references_walk_the_tree() { - let p = parse("any(crate(serde), not(crate(tokio)))").unwrap(); + let p = parse("any(depends-on(serde), not(depends-on(tokio)))").unwrap(); let mut names = std::collections::BTreeSet::new(); - p.collect_crate_names(&mut names); + p.collect_dep_names(&mut names); assert_eq!( names.into_iter().collect::>(), vec!["serde", "tokio"] ); - assert!(p.references_crate("serde")); - assert!(p.references_crate("tokio")); - assert!(!p.references_crate("anyhow")); + assert!(p.references_dep("serde")); + assert!(p.references_dep("tokio")); + assert!(!p.references_dep("anyhow")); } #[test] - fn has_concrete_crate() { + fn has_concrete_dep() { assert!( - PredicateSet::from_crates("serde") + PredicateSet::from_depends_on("serde") .unwrap() - .has_concrete_crate() + .has_concrete_dep() + ); + assert!( + !PredicateSet::from_depends_on("*") + .unwrap() + .has_concrete_dep() ); - assert!(!PredicateSet::from_crates("*").unwrap().has_concrete_crate()); assert!( !PredicateSet::parse("shell(true)") .unwrap() - .has_concrete_crate() + .has_concrete_dep() ); } #[test] - fn has_fetchable_crate() { - let fetchable = |s: &str| PredicateSet::parse(s).unwrap().has_fetchable_crate(); + fn has_fetchable_dep() { + let fetchable = |s: &str| PredicateSet::parse(s).unwrap().has_fetchable_dep(); // A crate in a positive position is fetchable... - assert!(fetchable("crate(serde)")); - assert!(fetchable("any(crate(serde), not(crate(legacy)))")); - assert!(fetchable("all(crate(serde), env(USE_SERDE))")); + assert!(fetchable("depends-on(serde)")); + assert!(fetchable("any(depends-on(serde), not(depends-on(legacy)))")); + assert!(fetchable("all(depends-on(serde), env(USE_SERDE))")); assert!( - PredicateSet::from_crates("serde") + PredicateSet::from_depends_on("serde") .unwrap() - .has_fetchable_crate() + .has_fetchable_dep() ); // ...but a crate only under `not(...)` is not (its witness is empty). - assert!(!fetchable("not(crate(legacy))")); - assert!(!fetchable("all(not(crate(a)), env(X))")); - // `not(not(crate(a)))` still cannot fetch: `Not` always yields an empty + assert!(!fetchable("not(depends-on(legacy))")); + assert!(!fetchable("all(not(depends-on(a)), env(X))")); + // `not(not(depends-on(a)))` still cannot fetch: `Not` always yields an empty // witness regardless of nesting depth. - assert!(!fetchable("not(not(crate(a)))")); + assert!(!fetchable("not(not(depends-on(a)))")); // Wildcards and non-crate leaves are never fetchable. assert!( - !PredicateSet::from_crates("*") + !PredicateSet::from_depends_on("*") .unwrap() - .has_fetchable_crate() + .has_fetchable_dep() ); assert!(!fetchable("shell(true)")); } @@ -1242,16 +1285,16 @@ mod tests { #[test] fn display_round_trip() { for input in [ - "crate(serde)", - "crate(serde>=1.0)", - "crate(*)", + "depends-on(serde)", + "depends-on(serde>=1.0)", + "depends-on(*)", "shell(command -v rg)", "path_exists(rg)", "env(CI)", "env(MODE=debug)", - "not(crate(serde))", - "any(crate(a), path_exists(b))", - "all(crate(a), not(env(CI)))", + "not(depends-on(serde))", + "any(depends-on(a), path_exists(b))", + "all(depends-on(a), not(env(CI)))", ] { let p = parse(input).unwrap(); assert_eq!(p.to_string(), input, "display drift: {input}"); @@ -1265,22 +1308,25 @@ mod tests { fn toml_fields_deserialize() { #[derive(serde::Deserialize)] struct Container { - #[serde(default)] - crates: CrateList, + #[serde(default, rename = "depends-on")] + depends_on: DependsOnList, #[serde(default)] predicates: PredicateSet, } let c: Container = toml::from_str( - r#"crates = ["serde", "tokio>=1.0"] - predicates = ["path_exists(jq)", "not(crate(foo))"]"#, + r#"depends-on = ["serde", "tokio>=1.0"] + predicates = ["path_exists(jq)", "not(depends-on(foo))"]"#, ) .unwrap(); - assert_eq!(c.crates.0.len(), 2); + assert_eq!(c.depends_on.0.len(), 2); assert_eq!(c.predicates.predicates.len(), 2); - // single-string crates form - let c2: Container = toml::from_str(r#"crates = "serde""#).unwrap(); - assert_eq!(c2.crates.0, vec![Predicate::Crate("serde".into(), None)]); + // single-string depends-on form + let c2: Container = toml::from_str(r#"depends-on = "serde""#).unwrap(); + assert_eq!( + c2.depends_on.0, + vec![Predicate::DependsOn("serde".into(), None)] + ); } // --- Custom predicate parsing tests --- @@ -1350,8 +1396,8 @@ mod tests { #[test] fn custom_not_confused_with_builtin() { - let p = parse("crate(serde)").unwrap(); - assert_eq!(p, Predicate::Crate("serde".into(), None)); + let p = parse("depends-on(serde)").unwrap(); + assert_eq!(p, Predicate::DependsOn("serde".into(), None)); } #[test] @@ -1360,7 +1406,7 @@ mod tests { name: "foo".into(), arg: "x".into(), }; - assert!(!p.has_concrete_crate()); + assert!(!p.has_concrete_dep()); } #[test] @@ -1369,7 +1415,7 @@ mod tests { name: "foo".into(), arg: "x".into(), }; - assert!(p.has_fetchable_crate()); + assert!(p.has_fetchable_dep()); } #[test] @@ -1378,7 +1424,7 @@ mod tests { name: "foo".into(), arg: "x".into(), }; - assert!(!p.mentions_crate()); + assert!(!p.mentions_dep()); } #[test] @@ -1387,8 +1433,8 @@ mod tests { name: "foo".into(), arg: "x".into(), }; - assert!(!p.references_crate("foo")); - assert!(!p.references_crate("x")); + assert!(!p.references_dep("foo")); + assert!(!p.references_dep("x")); } #[test] @@ -1398,7 +1444,7 @@ mod tests { arg: "x".into(), }; let mut names = std::collections::BTreeSet::new(); - p.collect_crate_names(&mut names); + p.collect_dep_names(&mut names); assert!(names.is_empty()); } diff --git a/src/skills.rs b/src/skills.rs index a6967d49..d8050425 100644 --- a/src/skills.rs +++ b/src/skills.rs @@ -27,8 +27,8 @@ fn source_display(source: &PluginSource) -> String { pub struct Skill { /// Frontmatter fields as key-value pairs (name, description, license, etc.). pub frontmatter: BTreeMap, - /// Skill-level activation predicates: the frontmatter `crates` (lowered to - /// `any(crate(...))`) merged with `predicates`. ANDed with the plugin- and + /// Skill-level activation predicates: the frontmatter `depends-on` (lowered to + /// `any(depends-on(...))`) merged with `predicates`. ANDed with the plugin- and /// group-level sets. pub predicates: PredicateSet, /// The body content (everything after frontmatter). @@ -220,7 +220,7 @@ pub async fn skills_applicable_to( /// Discover and load skills for a group, applying pre-fetch filtering. /// -/// Checks group-level `crates` predicates against `for_crates` before +/// Checks group-level `depends-on` predicates against `for_crates` before /// fetching git sources, to avoid unnecessary downloads. Each returned /// skill is paired with the `SkillOrigin` it was discovered through: /// @@ -304,15 +304,15 @@ async fn load_skills_for_group( /// recursively with cycle detection. /// /// The crates to fetch are the *witness* of the plugin- and group-level -/// predicate sets: the concrete crates that participate in satisfying the gate -/// (see [`predicate::union_matched_crates`]). +/// predicate sets: the concrete packages that participate in satisfying the +/// gate (see [`predicate::union_matched_packages`]). async fn load_crate_skills( plugin: &crate::plugins::Plugin, group: &SkillGroup, workspace_crates: &[symposium_sdk::workspace::WorkspaceCrate], ctx: &mut PredicateContext<'_>, ) -> Vec<(Skill, SkillOrigin)> { - let matched = predicate::union_matched_crates(&[&plugin.predicates, &group.predicates], ctx); + let matched = predicate::union_matched_packages(&[&plugin.predicates, &group.predicates], ctx); let mut skills = Vec::new(); for (name, _version) in &matched { let mut visited = std::collections::HashSet::new(); @@ -683,15 +683,15 @@ pub(crate) fn prune_nested_skills(paths: &mut Vec) { /// Load a standalone skill from a SKILL.md file (no plugin group context). /// -/// Standalone skills must be self-contained: all metadata (crates) +/// Standalone skills must be self-contained: all metadata (`depends-on`) /// comes from the SKILL.md frontmatter. -/// Returns an error if `crates` is missing (standalone skills have +/// Returns an error if `depends-on` is missing (standalone skills have /// no group to inherit from). pub fn load_standalone_skill(skill_md_path: &Path) -> Result { let skill = load_skill(skill_md_path, &SkillGroup::default())?; - if !skill.predicates.mentions_crate() { + if !skill.predicates.mentions_dep() { bail!( - "standalone skill `{}` is missing `crates` in frontmatter \ + "standalone skill `{}` is missing `depends-on` in frontmatter \ (standalone skills have no plugin group to inherit from)", skill.name() ); @@ -701,9 +701,9 @@ pub fn load_standalone_skill(skill_md_path: &Path) -> Result { /// Load a single skill from a SKILL.md file. /// -/// A skill should have `crates` at either the skill level or +/// A skill should have `depends-on` at either the skill level or /// the group level (or both). If neither provides it, a warning is logged -/// but loading succeeds (the skill simply won't match any crate query). +/// but loading succeeds (the skill simply won't match any dependency query). fn load_skill(skill_md_path: &Path, group: &SkillGroup) -> Result { let content = std::fs::read_to_string(skill_md_path) .with_context(|| format!("failed to read {}", skill_md_path.display()))?; @@ -740,26 +740,26 @@ fn load_skill(skill_md_path: &Path, group: &SkillGroup) -> Result { ); } - // Merge the skill-level `crates` (crate atoms, OR-combined) with the - // frontmatter `predicates` (function-call syntax) into one set, ANDed with - // the plugin- and group-level sets at match time. - let crates = match fm.crates.as_deref() { - Some(s) => Some(crate::predicate::CrateList::parse(s)?), + // Merge the skill-level `depends-on` (dependency atoms, OR-combined) with + // the frontmatter `predicates` (function-call syntax) into one set, ANDed + // with the plugin- and group-level sets at match time. + let depends_on = match fm.depends_on.as_deref() { + Some(s) => Some(crate::predicate::DependsOnList::parse(s)?), None => None, }; let extra = match fm.predicates.as_deref() { Some(s) => PredicateSet::parse(s)?, None => PredicateSet::default(), }; - let predicates = PredicateSet::merged(crates, extra); + let predicates = PredicateSet::merged(depends_on, extra); - // Warn if no crate is referenced at either level — the skill won't match - // any crate query, but we don't fail so a misconfigured plugin can't bring - // down the tool. - if !predicates.mentions_crate() && !group.predicates.mentions_crate() { + // Warn if no dependency is referenced at either level — the skill won't + // match any dependency query, but we don't fail so a misconfigured plugin + // can't bring down the tool. + if !predicates.mentions_dep() && !group.predicates.mentions_dep() { tracing::warn!( skill = %name, - "skill references no crate in SKILL.md frontmatter or its plugin [[skills]] group" + "skill references no dependency in SKILL.md frontmatter or its plugin [[skills]] group" ); } @@ -808,12 +808,12 @@ fn collect_skill_applicable_to( } /// Raw frontmatter fields extracted from a SKILL.md file. -/// `crates` is comma-separated on a single line. +/// `depends-on` is comma-separated on a single line. #[derive(Debug)] struct RawFrontmatter { fields: BTreeMap, - /// Raw `crates` value (comma-separated predicate string). - crates: Option, + /// Raw `depends-on` value (comma-separated predicate string). + depends_on: Option, /// Raw `predicates` value (comma-separated predicate expressions). predicates: Option, body: String, @@ -850,7 +850,7 @@ fn parse_frontmatter(content: &str) -> Result { .context("SKILL.md frontmatter must be a YAML mapping")?; let mut fields = BTreeMap::new(); - let mut crates = None; + let mut depends_on = None; let mut predicates = None; for (key, value) in mapping { @@ -863,7 +863,10 @@ fn parse_frontmatter(content: &str) -> Result { }; match key { - "crates" => crates = Some(value.to_string()), + "depends-on" => depends_on = Some(value.to_string()), + "crates" => { + bail!("the `crates` frontmatter field has been renamed; use `depends-on` instead") + } "predicates" => predicates = Some(value.to_string()), _ => { fields.insert(key.to_string(), value.to_string()); @@ -873,7 +876,7 @@ fn parse_frontmatter(content: &str) -> Result { Ok(RawFrontmatter { fields, - crates, + depends_on, predicates, body: body.to_string(), }) @@ -887,9 +890,9 @@ mod tests { use crate::predicate::Predicate; - /// Build a predicate set from crate atoms (the `crates` field form). + /// Build a predicate set from dependency atoms (the `depends-on` field form). fn pred_set(s: &str) -> PredicateSet { - PredicateSet::from_crates(s).unwrap() + PredicateSet::from_depends_on(s).unwrap() } fn ctx(crates: &[(String, semver::Version)]) -> PredicateContext<'_> { @@ -990,7 +993,7 @@ mod tests { --- name: my-skill description: A test skill - crates: serde + depends-on: serde --- # Body content @@ -1000,23 +1003,44 @@ mod tests { let fm = parse_frontmatter(content).unwrap(); assert_eq!(fm.fields.get("name").unwrap(), "my-skill"); assert_eq!(fm.fields.get("description").unwrap(), "A test skill"); - assert_eq!(fm.crates.as_deref(), Some("serde")); + assert_eq!(fm.depends_on.as_deref(), Some("serde")); assert!(fm.body.contains("# Body content")); assert!(fm.body.contains("Some instructions here.")); } #[test] - fn parse_frontmatter_comma_separated_crates() { + fn parse_frontmatter_comma_separated_depends_on() { let content = indoc! {" --- name: multi - crates: serde, serde_json>=1.0, toml + depends-on: serde, serde_json>=1.0, toml --- Body. "}; let fm = parse_frontmatter(content).unwrap(); - assert_eq!(fm.crates.as_deref(), Some("serde, serde_json>=1.0, toml")); + assert_eq!( + fm.depends_on.as_deref(), + Some("serde, serde_json>=1.0, toml") + ); + } + + #[test] + fn parse_frontmatter_rejects_renamed_crates_field() { + let content = indoc! {" + --- + name: old-spelling + description: Old spelling + crates: serde + --- + + Body. + "}; + let err = parse_frontmatter(content).unwrap_err(); + assert!( + err.to_string().contains("use `depends-on` instead"), + "expected migration hint, got: {err}" + ); } #[test] @@ -1080,7 +1104,7 @@ mod tests { --- name: test-skill description: Test - crates: serde + depends-on: serde --- Use serde like this. @@ -1092,7 +1116,7 @@ mod tests { let skill = load_skill(&skill_md, &defaults).unwrap(); assert_eq!(skill.frontmatter.get("name").unwrap(), "test-skill"); - assert!(skill.predicates.references_crate("serde")); + assert!(skill.predicates.references_dep("serde")); assert!(skill.body.contains("Use serde like this.")); } @@ -1106,7 +1130,7 @@ mod tests { --- name: multi-crate description: Multi-crate skill - crates: serde, tokio>=1.0 + depends-on: serde, tokio>=1.0 --- Body. @@ -1116,8 +1140,8 @@ mod tests { let defaults = SkillGroup::default(); let skill = load_skill(&skill_md, &defaults).unwrap(); - assert!(skill.predicates.references_crate("serde")); - assert!(skill.predicates.references_crate("tokio")); + assert!(skill.predicates.references_dep("serde")); + assert!(skill.predicates.references_dep("tokio")); } #[test] @@ -1158,7 +1182,7 @@ mod tests { --- name: override description: Override skill - crates: serde + depends-on: serde --- Body. @@ -1173,8 +1197,8 @@ mod tests { let skill = load_skill(&skill_md, &defaults).unwrap(); // Skill-level crates specializes (ANDs with) plugin defaults - assert!(skill.predicates.references_crate("serde")); - assert!(!skill.predicates.references_crate("tokio")); + assert!(skill.predicates.references_dep("serde")); + assert!(!skill.predicates.references_dep("tokio")); } #[test] @@ -1185,8 +1209,8 @@ mod tests { &skill_md, indoc! {" --- - name: no-crates - description: Missing crates + name: no-depends-on + description: Missing depends-on --- Body. @@ -1240,7 +1264,7 @@ mod tests { --- name: my-standalone description: A standalone skill - crates: serde + depends-on: serde --- Standalone body. @@ -1250,12 +1274,12 @@ mod tests { let skill = load_standalone_skill(&skill_dir.join("SKILL.md")).unwrap(); assert_eq!(skill.name(), "my-standalone"); - assert!(skill.predicates.references_crate("serde")); + assert!(skill.predicates.references_dep("serde")); assert!(skill.body.contains("Standalone body.")); } #[test] - fn validate_standalone_skill_bad_crates() { + fn validate_standalone_skill_bad_depends_on() { let tmp = tempfile::tempdir().unwrap(); let skill_dir = tmp.path().join("bad-skill"); fs::create_dir_all(&skill_dir).unwrap(); @@ -1264,8 +1288,8 @@ mod tests { indoc! {" --- name: bad - description: Bad crates skill - crates: \">=not_valid!!\" + description: Bad depends-on skill + depends-on: \">=not_valid!!\" --- Body. @@ -1275,7 +1299,7 @@ mod tests { let err = load_standalone_skill(&skill_dir.join("SKILL.md")).unwrap_err(); assert!( - err.to_string().contains("crate predicate"), + err.to_string().contains("depends-on predicate"), "expected parse error, got: {err}" ); } @@ -1412,7 +1436,7 @@ mod tests { --- name: serde-basics description: Basic serde usage - crates: serde + depends-on: serde --- Use derive macros. @@ -1486,7 +1510,7 @@ mod tests { --- name: serde-basics description: Basic serde usage - crates: serde + depends-on: serde --- Use derive macros. @@ -1499,7 +1523,7 @@ mod tests { name: "p".into(), predicates: PredicateSet { predicates: vec![ - Predicate::Crate("serde".into(), None), + Predicate::DependsOn("serde".into(), None), Predicate::Shell("false".into()), ], }, @@ -1562,7 +1586,7 @@ mod tests { --- name: serde-basics description: Basic serde usage - crates: serde + depends-on: serde --- Body. @@ -1574,7 +1598,7 @@ mod tests { name: "p".into(), predicates: PredicateSet { predicates: vec![ - Predicate::Crate("serde".into(), None), + Predicate::DependsOn("serde".into(), None), Predicate::Shell("true".into()), ], }, @@ -1582,7 +1606,7 @@ mod tests { skills: vec![SkillGroup { predicates: PredicateSet { predicates: vec![ - Predicate::Crate("serde".into(), None), + Predicate::DependsOn("serde".into(), None), Predicate::Shell("true".into()), ], }, @@ -1632,7 +1656,7 @@ mod tests { --- name: with-env description: A skill with runtime predicates - crates: serde + depends-on: serde predicates: shell(command -v rg), path_exists(Cargo.toml) --- @@ -1642,12 +1666,12 @@ mod tests { .unwrap(); let skill = load_skill(&skill_md, &SkillGroup::default()).unwrap(); - // `crates: serde` lowers to a leading `crate(serde)`, then the two + // `depends-on: serde` lowers to a leading `depends-on(serde)`, then the two // function-call predicates. assert_eq!( skill.predicates.predicates, vec![ - Predicate::Crate("serde".into(), None), + Predicate::DependsOn("serde".into(), None), Predicate::Shell("command -v rg".into()), Predicate::PathExists("Cargo.toml".into()), ] @@ -1663,7 +1687,7 @@ mod tests { skill_dir.join("SKILL.md"), indoc! {" --- - crates: serde + depends-on: serde --- Body. @@ -1679,16 +1703,16 @@ mod tests { } #[test] - fn standalone_skill_requires_crates() { + fn standalone_skill_requires_depends_on() { let tmp = tempfile::tempdir().unwrap(); - let skill_dir = tmp.path().join("no-crates"); + let skill_dir = tmp.path().join("no-depends-on"); fs::create_dir_all(&skill_dir).unwrap(); fs::write( skill_dir.join("SKILL.md"), indoc! {" --- - name: no-crates - description: Missing crates + name: no-depends-on + description: Missing depends-on --- Body. @@ -1698,8 +1722,8 @@ mod tests { let err = load_standalone_skill(&skill_dir.join("SKILL.md")).unwrap_err(); assert!( - err.to_string().contains("missing `crates`"), - "expected crates error, got: {err}" + err.to_string().contains("missing `depends-on`"), + "expected depends-on error, got: {err}" ); } @@ -1716,7 +1740,7 @@ mod tests { --- name: standalone-serde description: Standalone serde skill - crates: serde + depends-on: serde --- Body. @@ -1754,7 +1778,7 @@ mod tests { .await; assert_eq!(results.len(), 1); assert_eq!(results[0].skill.name(), "standalone-serde"); - assert!(results[0].skill.predicates.references_crate("serde")); + assert!(results[0].skill.predicates.references_dep("serde")); } // --- Discovery --- @@ -1773,7 +1797,7 @@ mod tests { --- name: my-skill description: A discovered skill - crates: serde + depends-on: serde --- Discovered body. @@ -1803,7 +1827,7 @@ mod tests { --- name: nested-skill description: Nested skill - crates: tokio + depends-on: tokio --- Nested body. @@ -1833,7 +1857,7 @@ mod tests { --- name: shallow description: Shallow skill - crates: serde + depends-on: serde --- Shallow. @@ -1850,7 +1874,7 @@ mod tests { --- name: nested description: Nested skill - crates: serde + depends-on: serde --- Nested. @@ -1867,7 +1891,7 @@ mod tests { --- name: sibling description: Sibling skill - crates: tokio + depends-on: tokio --- Sibling. @@ -1903,7 +1927,7 @@ mod tests { semver::Version::parse(s).unwrap() } - /// Each level's `crates` lowers to one predicate set; the skill applies when + /// Each level's `depends-on` lowers to one predicate set; the skill applies when /// every level's set holds (AND across levels). fn applies(levels: &[&str], ws: &[(String, semver::Version)]) -> bool { levels diff --git a/src/subcommand_dispatch.rs b/src/subcommand_dispatch.rs index 23fcb7b4..0bcb4c6b 100644 --- a/src/subcommand_dispatch.rs +++ b/src/subcommand_dispatch.rs @@ -166,19 +166,19 @@ mod tests { } fn crate_set(spec: &str) -> PredicateSet { - PredicateSet::from_crates(spec).unwrap() + PredicateSet::from_depends_on(spec).unwrap() } fn plugin_with( name: &str, - crates: &str, + depends_on: &str, subcommands: BTreeMap, ) -> ParsedPlugin { ParsedPlugin { path: PathBuf::from(format!("/test/{name}.toml")), plugin: Plugin { name: name.into(), - predicates: crate_set(crates), + predicates: crate_set(depends_on), installations: vec![], hooks: vec![], skills: vec![], @@ -191,12 +191,12 @@ mod tests { } } - fn subcommand(command: &str, crates: Option<&str>) -> Subcommand { + fn subcommand(command: &str, depends_on: Option<&str>) -> Subcommand { Subcommand { description: "test".into(), audience: Audience::default(), command: command.into(), - predicates: crates.map(crate_set).unwrap_or_default(), + predicates: depends_on.map(crate_set).unwrap_or_default(), } } diff --git a/src/test_utils.rs b/src/test_utils.rs index 8441bd4f..8d8f1082 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -14,7 +14,7 @@ pub struct File<'a>(pub &'a str, pub &'a str); /// /// ```ignore /// let tmp = instantiate_fixture(&[ -/// File("foo/SYMPOSIUM.toml", r#"name = "foo"\ncrates = ["*"]"#), +/// File("foo/SYMPOSIUM.toml", r#"name = "foo"\ndepends-on = ["*"]"#), /// File("bar/SKILL.md", "---\nname: bar\n---\nBody."), /// ]); /// let root = tmp.path(); diff --git a/test_structure.rs b/test_structure.rs index b3755a07..8da5ec7f 100644 --- a/test_structure.rs +++ b/test_structure.rs @@ -18,7 +18,7 @@ fn main() { skill_dir.join("SKILL.md"), r#"--- name: my-skill -crates: serde +depends-on: serde --- Test skill."#, @@ -30,7 +30,7 @@ Test skill."#, mixed_dir.join("SKILL.md"), r#"--- name: mixed-skill -crates: tokio +depends-on: tokio --- Mixed skill."#, diff --git a/tests/fixtures/crate-path-bad-metadata0/dot-symposium/plugins/bad-metadata-plugin/SYMPOSIUM.toml b/tests/fixtures/crate-path-bad-metadata0/dot-symposium/plugins/bad-metadata-plugin/SYMPOSIUM.toml index 81694897..067b5f34 100644 --- a/tests/fixtures/crate-path-bad-metadata0/dot-symposium/plugins/bad-metadata-plugin/SYMPOSIUM.toml +++ b/tests/fixtures/crate-path-bad-metadata0/dot-symposium/plugins/bad-metadata-plugin/SYMPOSIUM.toml @@ -1,5 +1,5 @@ name = "bad-metadata-plugin" -crates = ["crate-bad"] +depends-on = ["crate-bad"] [[skills]] source = "crate" diff --git a/tests/fixtures/crate-path-cycle0/dot-symposium/plugins/cycle-plugin/SYMPOSIUM.toml b/tests/fixtures/crate-path-cycle0/dot-symposium/plugins/cycle-plugin/SYMPOSIUM.toml index f6d672ae..946853f8 100644 --- a/tests/fixtures/crate-path-cycle0/dot-symposium/plugins/cycle-plugin/SYMPOSIUM.toml +++ b/tests/fixtures/crate-path-cycle0/dot-symposium/plugins/cycle-plugin/SYMPOSIUM.toml @@ -1,5 +1,5 @@ name = "cycle-plugin" -crates = ["crate-a"] +depends-on = ["crate-a"] [[skills]] source = "crate" diff --git a/tests/fixtures/crate-path-diamond0/dot-symposium/plugins/diamond-plugin/SYMPOSIUM.toml b/tests/fixtures/crate-path-diamond0/dot-symposium/plugins/diamond-plugin/SYMPOSIUM.toml index 998c284c..0f348c5e 100644 --- a/tests/fixtures/crate-path-diamond0/dot-symposium/plugins/diamond-plugin/SYMPOSIUM.toml +++ b/tests/fixtures/crate-path-diamond0/dot-symposium/plugins/diamond-plugin/SYMPOSIUM.toml @@ -1,5 +1,5 @@ name = "diamond-plugin" -crates = ["crate-a", "crate-b"] +depends-on = ["crate-a", "crate-b"] [[skills]] source = "crate" diff --git a/tests/fixtures/crate-path-hyphen-cycle0/dot-symposium/plugins/hyphen-plugin/SYMPOSIUM.toml b/tests/fixtures/crate-path-hyphen-cycle0/dot-symposium/plugins/hyphen-plugin/SYMPOSIUM.toml index cfc478d0..082e6055 100644 --- a/tests/fixtures/crate-path-hyphen-cycle0/dot-symposium/plugins/hyphen-plugin/SYMPOSIUM.toml +++ b/tests/fixtures/crate-path-hyphen-cycle0/dot-symposium/plugins/hyphen-plugin/SYMPOSIUM.toml @@ -1,5 +1,5 @@ name = "hyphen-plugin" -crates = ["crate-foo"] +depends-on = ["crate-foo"] [[skills]] source = "crate" diff --git a/tests/fixtures/crate-path-missing-dir0/dot-symposium/plugins/missing-dir-plugin/SYMPOSIUM.toml b/tests/fixtures/crate-path-missing-dir0/dot-symposium/plugins/missing-dir-plugin/SYMPOSIUM.toml index 7202d759..2200401b 100644 --- a/tests/fixtures/crate-path-missing-dir0/dot-symposium/plugins/missing-dir-plugin/SYMPOSIUM.toml +++ b/tests/fixtures/crate-path-missing-dir0/dot-symposium/plugins/missing-dir-plugin/SYMPOSIUM.toml @@ -1,5 +1,5 @@ name = "missing-dir-plugin" -crates = ["crate-miss"] +depends-on = ["crate-miss"] [[skills]] source = "crate" diff --git a/tests/fixtures/crate-path-multi-entry0/dot-symposium/plugins/multi-entry-plugin/SYMPOSIUM.toml b/tests/fixtures/crate-path-multi-entry0/dot-symposium/plugins/multi-entry-plugin/SYMPOSIUM.toml index 6b72d0cf..68bc2973 100644 --- a/tests/fixtures/crate-path-multi-entry0/dot-symposium/plugins/multi-entry-plugin/SYMPOSIUM.toml +++ b/tests/fixtures/crate-path-multi-entry0/dot-symposium/plugins/multi-entry-plugin/SYMPOSIUM.toml @@ -1,5 +1,5 @@ name = "multi-entry-plugin" -crates = ["crate-m"] +depends-on = ["crate-m"] [[skills]] source = "crate" diff --git a/tests/fixtures/crate-path-multihop0/dot-symposium/plugins/multihop-plugin/SYMPOSIUM.toml b/tests/fixtures/crate-path-multihop0/dot-symposium/plugins/multihop-plugin/SYMPOSIUM.toml index 43a0f31a..93fd7deb 100644 --- a/tests/fixtures/crate-path-multihop0/dot-symposium/plugins/multihop-plugin/SYMPOSIUM.toml +++ b/tests/fixtures/crate-path-multihop0/dot-symposium/plugins/multihop-plugin/SYMPOSIUM.toml @@ -1,5 +1,5 @@ name = "multihop-plugin" -crates = ["crate-a"] +depends-on = ["crate-a"] [[skills]] source = "crate" diff --git a/tests/fixtures/crate-path-named0/dot-symposium/plugins/named-crate-plugin/SYMPOSIUM.toml b/tests/fixtures/crate-path-named0/dot-symposium/plugins/named-crate-plugin/SYMPOSIUM.toml index 90e5108d..1e13ad9f 100644 --- a/tests/fixtures/crate-path-named0/dot-symposium/plugins/named-crate-plugin/SYMPOSIUM.toml +++ b/tests/fixtures/crate-path-named0/dot-symposium/plugins/named-crate-plugin/SYMPOSIUM.toml @@ -1,5 +1,5 @@ name = "named-crate-plugin" -crates = ["crate-a"] +depends-on = ["crate-a"] [[skills]] source = "crate" diff --git a/tests/fixtures/crate-path-optout0/dot-symposium/plugins/optout-plugin/SYMPOSIUM.toml b/tests/fixtures/crate-path-optout0/dot-symposium/plugins/optout-plugin/SYMPOSIUM.toml index 6421b579..122de5f0 100644 --- a/tests/fixtures/crate-path-optout0/dot-symposium/plugins/optout-plugin/SYMPOSIUM.toml +++ b/tests/fixtures/crate-path-optout0/dot-symposium/plugins/optout-plugin/SYMPOSIUM.toml @@ -1,5 +1,5 @@ name = "optout-plugin" -crates = ["crate-opt"] +depends-on = ["crate-opt"] [[skills]] source = "crate" diff --git a/tests/fixtures/crate-path-redirect-optout0/dot-symposium/plugins/redirect-optout-plugin/SYMPOSIUM.toml b/tests/fixtures/crate-path-redirect-optout0/dot-symposium/plugins/redirect-optout-plugin/SYMPOSIUM.toml index d7e64ad1..694e44df 100644 --- a/tests/fixtures/crate-path-redirect-optout0/dot-symposium/plugins/redirect-optout-plugin/SYMPOSIUM.toml +++ b/tests/fixtures/crate-path-redirect-optout0/dot-symposium/plugins/redirect-optout-plugin/SYMPOSIUM.toml @@ -1,5 +1,5 @@ name = "redirect-optout-plugin" -crates = ["crate-a"] +depends-on = ["crate-a"] [[skills]] source = "crate" diff --git a/tests/fixtures/crate-path0/crate-x/skills/x-guidance/SKILL.md b/tests/fixtures/crate-path0/crate-x/skills/x-guidance/SKILL.md index 8882da4d..90c2c775 100644 --- a/tests/fixtures/crate-path0/crate-x/skills/x-guidance/SKILL.md +++ b/tests/fixtures/crate-path0/crate-x/skills/x-guidance/SKILL.md @@ -1,7 +1,7 @@ --- name: x-guidance description: Guidance for using crate-x -crates: crate-x +depends-on: crate-x --- Use crate-x like this. diff --git a/tests/fixtures/crate-path0/crate-z/guidance/z-guidance/SKILL.md b/tests/fixtures/crate-path0/crate-z/guidance/z-guidance/SKILL.md index 79895755..5fedad8a 100644 --- a/tests/fixtures/crate-path0/crate-z/guidance/z-guidance/SKILL.md +++ b/tests/fixtures/crate-path0/crate-z/guidance/z-guidance/SKILL.md @@ -1,7 +1,7 @@ --- name: z-guidance description: Guidance for using crate-z -crates: crate-z +depends-on: crate-z --- Use crate-z like this. diff --git a/tests/fixtures/crate-path0/dot-symposium/plugins/crate-x-plugin/SYMPOSIUM.toml b/tests/fixtures/crate-path0/dot-symposium/plugins/crate-x-plugin/SYMPOSIUM.toml index ecfafba3..95b2631f 100644 --- a/tests/fixtures/crate-path0/dot-symposium/plugins/crate-x-plugin/SYMPOSIUM.toml +++ b/tests/fixtures/crate-path0/dot-symposium/plugins/crate-x-plugin/SYMPOSIUM.toml @@ -1,5 +1,5 @@ name = "crate-x-plugin" -crates = ["crate-x"] +depends-on = ["crate-x"] [[skills]] source = "crate" diff --git a/tests/fixtures/crate-path0/dot-symposium/plugins/crate-z-plugin/SYMPOSIUM.toml b/tests/fixtures/crate-path0/dot-symposium/plugins/crate-z-plugin/SYMPOSIUM.toml index 2c1b0d9a..7c5890a8 100644 --- a/tests/fixtures/crate-path0/dot-symposium/plugins/crate-z-plugin/SYMPOSIUM.toml +++ b/tests/fixtures/crate-path0/dot-symposium/plugins/crate-z-plugin/SYMPOSIUM.toml @@ -1,5 +1,5 @@ name = "crate-z-plugin" -crates = ["crate-z"] +depends-on = ["crate-z"] [[skills]] source = "crate" diff --git a/tests/fixtures/custom-predicate-cross0/dot-symposium/plugins/provider-plugin/SYMPOSIUM.toml b/tests/fixtures/custom-predicate-cross0/dot-symposium/plugins/provider-plugin/SYMPOSIUM.toml index a17acc7e..98a3a651 100644 --- a/tests/fixtures/custom-predicate-cross0/dot-symposium/plugins/provider-plugin/SYMPOSIUM.toml +++ b/tests/fixtures/custom-predicate-cross0/dot-symposium/plugins/provider-plugin/SYMPOSIUM.toml @@ -1,5 +1,5 @@ name = "provider-plugin" -crates = ["*"] +depends-on = ["*"] [[installations]] name = "my-checker" diff --git a/tests/fixtures/dedup-crate-origin0/crate-x/skills/code-review/SKILL.md b/tests/fixtures/dedup-crate-origin0/crate-x/skills/code-review/SKILL.md index 853918d2..59f93095 100644 --- a/tests/fixtures/dedup-crate-origin0/crate-x/skills/code-review/SKILL.md +++ b/tests/fixtures/dedup-crate-origin0/crate-x/skills/code-review/SKILL.md @@ -1,7 +1,7 @@ --- name: code-review description: How to review crate-x code -crates: crate-x +depends-on: crate-x --- Review crate-x like this. diff --git a/tests/fixtures/dedup-crate-origin0/dot-symposium/plugins/plugin-a/SYMPOSIUM.toml b/tests/fixtures/dedup-crate-origin0/dot-symposium/plugins/plugin-a/SYMPOSIUM.toml index dd1d6e25..2e69f201 100644 --- a/tests/fixtures/dedup-crate-origin0/dot-symposium/plugins/plugin-a/SYMPOSIUM.toml +++ b/tests/fixtures/dedup-crate-origin0/dot-symposium/plugins/plugin-a/SYMPOSIUM.toml @@ -1,5 +1,5 @@ name = "plugin-a" -crates = ["crate-x"] +depends-on = ["crate-x"] [[skills]] source = "crate" diff --git a/tests/fixtures/dedup-crate-origin0/dot-symposium/plugins/plugin-b/SYMPOSIUM.toml b/tests/fixtures/dedup-crate-origin0/dot-symposium/plugins/plugin-b/SYMPOSIUM.toml index c777e861..a4e43345 100644 --- a/tests/fixtures/dedup-crate-origin0/dot-symposium/plugins/plugin-b/SYMPOSIUM.toml +++ b/tests/fixtures/dedup-crate-origin0/dot-symposium/plugins/plugin-b/SYMPOSIUM.toml @@ -1,5 +1,5 @@ name = "plugin-b" -crates = ["crate-x"] +depends-on = ["crate-x"] [[skills]] source = "crate" diff --git a/tests/fixtures/dedup-source-origin0/dot-symposium/plugins/plugin-a/SYMPOSIUM.toml b/tests/fixtures/dedup-source-origin0/dot-symposium/plugins/plugin-a/SYMPOSIUM.toml index e9eff4bf..602105b3 100644 --- a/tests/fixtures/dedup-source-origin0/dot-symposium/plugins/plugin-a/SYMPOSIUM.toml +++ b/tests/fixtures/dedup-source-origin0/dot-symposium/plugins/plugin-a/SYMPOSIUM.toml @@ -1,6 +1,6 @@ name = "plugin-a" -crates = ["serde"] +depends-on = ["serde"] [[skills]] -crates = ["serde"] +depends-on = ["serde"] source.path = "../shared/the-skill" diff --git a/tests/fixtures/dedup-source-origin0/dot-symposium/plugins/plugin-b/SYMPOSIUM.toml b/tests/fixtures/dedup-source-origin0/dot-symposium/plugins/plugin-b/SYMPOSIUM.toml index beb8ba0a..31d01ade 100644 --- a/tests/fixtures/dedup-source-origin0/dot-symposium/plugins/plugin-b/SYMPOSIUM.toml +++ b/tests/fixtures/dedup-source-origin0/dot-symposium/plugins/plugin-b/SYMPOSIUM.toml @@ -1,6 +1,6 @@ name = "plugin-b" -crates = ["serde"] +depends-on = ["serde"] [[skills]] -crates = ["serde"] +depends-on = ["serde"] source.path = "../shared/the-skill" diff --git a/tests/fixtures/dedup-source-origin0/dot-symposium/plugins/shared/the-skill/SKILL.md b/tests/fixtures/dedup-source-origin0/dot-symposium/plugins/shared/the-skill/SKILL.md index a3e5ae1a..05efa502 100644 --- a/tests/fixtures/dedup-source-origin0/dot-symposium/plugins/shared/the-skill/SKILL.md +++ b/tests/fixtures/dedup-source-origin0/dot-symposium/plugins/shared/the-skill/SKILL.md @@ -1,7 +1,7 @@ --- name: shared-skill description: A skill bundled in shared/ and pointed at by two plugins -crates: serde +depends-on: serde --- The shared bytes. diff --git a/tests/fixtures/distinct-plugin-origins0/dot-symposium/plugins/plugin-a/SYMPOSIUM.toml b/tests/fixtures/distinct-plugin-origins0/dot-symposium/plugins/plugin-a/SYMPOSIUM.toml index 8f444638..93721040 100644 --- a/tests/fixtures/distinct-plugin-origins0/dot-symposium/plugins/plugin-a/SYMPOSIUM.toml +++ b/tests/fixtures/distinct-plugin-origins0/dot-symposium/plugins/plugin-a/SYMPOSIUM.toml @@ -1,6 +1,6 @@ name = "plugin-a" -crates = ["serde"] +depends-on = ["serde"] [[skills]] -crates = ["serde"] +depends-on = ["serde"] source.path = "." diff --git a/tests/fixtures/distinct-plugin-origins0/dot-symposium/plugins/plugin-a/code-review/SKILL.md b/tests/fixtures/distinct-plugin-origins0/dot-symposium/plugins/plugin-a/code-review/SKILL.md index efaf6fec..8e0b57de 100644 --- a/tests/fixtures/distinct-plugin-origins0/dot-symposium/plugins/plugin-a/code-review/SKILL.md +++ b/tests/fixtures/distinct-plugin-origins0/dot-symposium/plugins/plugin-a/code-review/SKILL.md @@ -1,7 +1,7 @@ --- name: code-review description: Plugin-A code review guidance -crates: serde +depends-on: serde --- Plugin-A says: review serde code this way. diff --git a/tests/fixtures/distinct-plugin-origins0/dot-symposium/plugins/plugin-b/SYMPOSIUM.toml b/tests/fixtures/distinct-plugin-origins0/dot-symposium/plugins/plugin-b/SYMPOSIUM.toml index 82f6eb05..58c5358b 100644 --- a/tests/fixtures/distinct-plugin-origins0/dot-symposium/plugins/plugin-b/SYMPOSIUM.toml +++ b/tests/fixtures/distinct-plugin-origins0/dot-symposium/plugins/plugin-b/SYMPOSIUM.toml @@ -1,6 +1,6 @@ name = "plugin-b" -crates = ["serde"] +depends-on = ["serde"] [[skills]] -crates = ["serde"] +depends-on = ["serde"] source.path = "." diff --git a/tests/fixtures/distinct-plugin-origins0/dot-symposium/plugins/plugin-b/code-review/SKILL.md b/tests/fixtures/distinct-plugin-origins0/dot-symposium/plugins/plugin-b/code-review/SKILL.md index 7e3f2c06..579dd09d 100644 --- a/tests/fixtures/distinct-plugin-origins0/dot-symposium/plugins/plugin-b/code-review/SKILL.md +++ b/tests/fixtures/distinct-plugin-origins0/dot-symposium/plugins/plugin-b/code-review/SKILL.md @@ -1,7 +1,7 @@ --- name: code-review description: Plugin-B code review guidance -crates: serde +depends-on: serde --- Plugin-B says: review serde code that way. diff --git a/tests/fixtures/distinct-standalone-paths0/dot-symposium/plugins/bar/my-skill/SKILL.md b/tests/fixtures/distinct-standalone-paths0/dot-symposium/plugins/bar/my-skill/SKILL.md index 1e5fe3fb..9dd2ec57 100644 --- a/tests/fixtures/distinct-standalone-paths0/dot-symposium/plugins/bar/my-skill/SKILL.md +++ b/tests/fixtures/distinct-standalone-paths0/dot-symposium/plugins/bar/my-skill/SKILL.md @@ -1,7 +1,7 @@ --- name: my-skill description: Bar flavor of my-skill -crates: serde +depends-on: serde --- Bar body. diff --git a/tests/fixtures/distinct-standalone-paths0/dot-symposium/plugins/foo/my-skill/SKILL.md b/tests/fixtures/distinct-standalone-paths0/dot-symposium/plugins/foo/my-skill/SKILL.md index e25917fd..0a24a430 100644 --- a/tests/fixtures/distinct-standalone-paths0/dot-symposium/plugins/foo/my-skill/SKILL.md +++ b/tests/fixtures/distinct-standalone-paths0/dot-symposium/plugins/foo/my-skill/SKILL.md @@ -1,7 +1,7 @@ --- name: my-skill description: Foo flavor of my-skill -crates: serde +depends-on: serde --- Foo body. diff --git a/tests/fixtures/help_render0/dot-symposium/plugins/example-plugin/SYMPOSIUM.toml b/tests/fixtures/help_render0/dot-symposium/plugins/example-plugin/SYMPOSIUM.toml index c1675f3a..8292fa48 100644 --- a/tests/fixtures/help_render0/dot-symposium/plugins/example-plugin/SYMPOSIUM.toml +++ b/tests/fixtures/help_render0/dot-symposium/plugins/example-plugin/SYMPOSIUM.toml @@ -1,5 +1,5 @@ name = "example-plugin" -crates = ["*"] +depends-on = ["*"] [[installations]] name = "example-tool-install" diff --git a/tests/fixtures/invalid-skill0/dot-symposium/plugins/bad-skill/SKILL.md b/tests/fixtures/invalid-skill0/dot-symposium/plugins/bad-skill/SKILL.md index d91953b8..c303dd87 100644 --- a/tests/fixtures/invalid-skill0/dot-symposium/plugins/bad-skill/SKILL.md +++ b/tests/fixtures/invalid-skill0/dot-symposium/plugins/bad-skill/SKILL.md @@ -1,7 +1,7 @@ --- name: rust-best-practice description: [Critical] Best practice for Rust coding. -crates: serde +depends-on: serde --- This should not be installed because the frontmatter is invalid YAML. diff --git a/tests/fixtures/mcp-filtering0/dot-symposium/plugins/mcp-plugin/SYMPOSIUM.toml b/tests/fixtures/mcp-filtering0/dot-symposium/plugins/mcp-plugin/SYMPOSIUM.toml index 3a1ee90c..1217c3e8 100644 --- a/tests/fixtures/mcp-filtering0/dot-symposium/plugins/mcp-plugin/SYMPOSIUM.toml +++ b/tests/fixtures/mcp-filtering0/dot-symposium/plugins/mcp-plugin/SYMPOSIUM.toml @@ -1,23 +1,23 @@ name = "mcp-plugin" -crates = ["*"] +depends-on = ["*"] [[mcp_servers]] name = "always-server" -crates = ["*"] +depends-on = ["*"] command = "/usr/bin/true" args = ["--stdio"] env = [] [[mcp_servers]] name = "serde-server" -crates = ["serde"] +depends-on = ["serde"] command = "/usr/bin/true" args = ["--stdio"] env = [] [[mcp_servers]] name = "missing-crate-server" -crates = ["reqwest"] +depends-on = ["reqwest"] command = "/usr/bin/true" args = ["--stdio"] env = [] diff --git a/tests/fixtures/multi-group-plugin0/dot-symposium/plugins/multi-plugin/SYMPOSIUM.toml b/tests/fixtures/multi-group-plugin0/dot-symposium/plugins/multi-plugin/SYMPOSIUM.toml index 078464d2..7dce3366 100644 --- a/tests/fixtures/multi-group-plugin0/dot-symposium/plugins/multi-plugin/SYMPOSIUM.toml +++ b/tests/fixtures/multi-group-plugin0/dot-symposium/plugins/multi-plugin/SYMPOSIUM.toml @@ -1,10 +1,10 @@ name = "multi-plugin" -crates = ["serde"] +depends-on = ["serde"] [[skills]] -crates = ["serde"] +depends-on = ["serde"] source.path = "group-a" [[skills]] -crates = ["serde"] +depends-on = ["serde"] source.path = "group-b" diff --git a/tests/fixtures/multi-group-plugin0/dot-symposium/plugins/multi-plugin/group-a/SKILL.md b/tests/fixtures/multi-group-plugin0/dot-symposium/plugins/multi-plugin/group-a/SKILL.md index 776f283a..a99b63fe 100644 --- a/tests/fixtures/multi-group-plugin0/dot-symposium/plugins/multi-plugin/group-a/SKILL.md +++ b/tests/fixtures/multi-group-plugin0/dot-symposium/plugins/multi-plugin/group-a/SKILL.md @@ -1,7 +1,7 @@ --- name: shared-name description: Group-A flavor of shared-name -crates: serde +depends-on: serde --- Group-A body. diff --git a/tests/fixtures/multi-group-plugin0/dot-symposium/plugins/multi-plugin/group-b/SKILL.md b/tests/fixtures/multi-group-plugin0/dot-symposium/plugins/multi-plugin/group-b/SKILL.md index ff7139e7..dcbc82a1 100644 --- a/tests/fixtures/multi-group-plugin0/dot-symposium/plugins/multi-plugin/group-b/SKILL.md +++ b/tests/fixtures/multi-group-plugin0/dot-symposium/plugins/multi-plugin/group-b/SKILL.md @@ -1,7 +1,7 @@ --- name: shared-name description: Group-B flavor of shared-name -crates: serde +depends-on: serde --- Group-B body. diff --git a/tests/fixtures/patch-crate0/crate-x/skills/x-patched-guidance/SKILL.md b/tests/fixtures/patch-crate0/crate-x/skills/x-patched-guidance/SKILL.md index 902992cf..c637d6f7 100644 --- a/tests/fixtures/patch-crate0/crate-x/skills/x-patched-guidance/SKILL.md +++ b/tests/fixtures/patch-crate0/crate-x/skills/x-patched-guidance/SKILL.md @@ -1,7 +1,7 @@ --- name: x-patched-guidance description: Guidance for using crate-x (patched) -crates: crate-x +depends-on: crate-x --- Use patched crate-x like this. diff --git a/tests/fixtures/patch-crate0/dot-symposium/plugins/crate-x-plugin/SYMPOSIUM.toml b/tests/fixtures/patch-crate0/dot-symposium/plugins/crate-x-plugin/SYMPOSIUM.toml index ecfafba3..95b2631f 100644 --- a/tests/fixtures/patch-crate0/dot-symposium/plugins/crate-x-plugin/SYMPOSIUM.toml +++ b/tests/fixtures/patch-crate0/dot-symposium/plugins/crate-x-plugin/SYMPOSIUM.toml @@ -1,5 +1,5 @@ name = "crate-x-plugin" -crates = ["crate-x"] +depends-on = ["crate-x"] [[skills]] source = "crate" diff --git a/tests/fixtures/plugin-hooks-format/dot-symposium/plugins/claude-only-plugin/SYMPOSIUM.toml b/tests/fixtures/plugin-hooks-format/dot-symposium/plugins/claude-only-plugin/SYMPOSIUM.toml index 2a7bfc15..5af5ff4a 100644 --- a/tests/fixtures/plugin-hooks-format/dot-symposium/plugins/claude-only-plugin/SYMPOSIUM.toml +++ b/tests/fixtures/plugin-hooks-format/dot-symposium/plugins/claude-only-plugin/SYMPOSIUM.toml @@ -1,5 +1,5 @@ name = "claude-only-plugin" -crates = ["*"] +depends-on = ["*"] # Only a Claude hook — no symposium fallback. # Should fire on Claude, should NOT fire on other agents. diff --git a/tests/fixtures/plugin-hooks-format/dot-symposium/plugins/format-plugin/SYMPOSIUM.toml b/tests/fixtures/plugin-hooks-format/dot-symposium/plugins/format-plugin/SYMPOSIUM.toml index ecc67589..b8f05f5d 100644 --- a/tests/fixtures/plugin-hooks-format/dot-symposium/plugins/format-plugin/SYMPOSIUM.toml +++ b/tests/fixtures/plugin-hooks-format/dot-symposium/plugins/format-plugin/SYMPOSIUM.toml @@ -1,5 +1,5 @@ name = "format-plugin" -crates = ["*"] +depends-on = ["*"] # Claude-specific hook — should fire on Claude, be skipped on other agents. [[hooks]] diff --git a/tests/fixtures/plugin-hooks0/dot-symposium/plugins/test-plugin/SYMPOSIUM.toml b/tests/fixtures/plugin-hooks0/dot-symposium/plugins/test-plugin/SYMPOSIUM.toml index 76b3ace2..c75e36d1 100644 --- a/tests/fixtures/plugin-hooks0/dot-symposium/plugins/test-plugin/SYMPOSIUM.toml +++ b/tests/fixtures/plugin-hooks0/dot-symposium/plugins/test-plugin/SYMPOSIUM.toml @@ -1,5 +1,5 @@ name = "test-plugin" -crates = ["*"] +depends-on = ["*"] # A reusable installation referenced by name. No source — `script` points to a # file on disk (variable-expanded by the fixture loader). diff --git a/tests/fixtures/plugin-skill-group0/dot-symposium/plugins/my-plugin/SYMPOSIUM.toml b/tests/fixtures/plugin-skill-group0/dot-symposium/plugins/my-plugin/SYMPOSIUM.toml index d8198580..14fbca06 100644 --- a/tests/fixtures/plugin-skill-group0/dot-symposium/plugins/my-plugin/SYMPOSIUM.toml +++ b/tests/fixtures/plugin-skill-group0/dot-symposium/plugins/my-plugin/SYMPOSIUM.toml @@ -1,6 +1,6 @@ name = "my-plugin" -crates = ["serde"] +depends-on = ["serde"] [[skills]] -crates = ["serde"] +depends-on = ["serde"] source.path = "." diff --git a/tests/fixtures/plugin-skill-group0/dot-symposium/plugins/wildcard-plugin/SYMPOSIUM.toml b/tests/fixtures/plugin-skill-group0/dot-symposium/plugins/wildcard-plugin/SYMPOSIUM.toml index 9166d3c4..e4b45de8 100644 --- a/tests/fixtures/plugin-skill-group0/dot-symposium/plugins/wildcard-plugin/SYMPOSIUM.toml +++ b/tests/fixtures/plugin-skill-group0/dot-symposium/plugins/wildcard-plugin/SYMPOSIUM.toml @@ -1,5 +1,5 @@ name = "wildcard-plugin" -crates = ["*"] +depends-on = ["*"] [[skills]] source.path = "." diff --git a/tests/fixtures/plugin-tokio-weather0/dot-symposium/plugins/skills/tokio-weather/SKILL.md b/tests/fixtures/plugin-tokio-weather0/dot-symposium/plugins/skills/tokio-weather/SKILL.md index c621279d..0003f304 100644 --- a/tests/fixtures/plugin-tokio-weather0/dot-symposium/plugins/skills/tokio-weather/SKILL.md +++ b/tests/fixtures/plugin-tokio-weather0/dot-symposium/plugins/skills/tokio-weather/SKILL.md @@ -1,7 +1,7 @@ --- name: tokio-weather description: Provides an answer to the question "What is the weather in tokio" -crates: tokio +depends-on: tokio --- *When the user asks the question "What is the weather in `tokio`", respond with this exact sentence, all capitalized, to demonstrate that you read this skill:* diff --git a/tests/fixtures/plugins0/dot-symposium/plugins/my-skill/SKILL.md b/tests/fixtures/plugins0/dot-symposium/plugins/my-skill/SKILL.md index 8b1816e4..bcdbf42d 100644 --- a/tests/fixtures/plugins0/dot-symposium/plugins/my-skill/SKILL.md +++ b/tests/fixtures/plugins0/dot-symposium/plugins/my-skill/SKILL.md @@ -1,7 +1,7 @@ --- name: serde-guidance description: Guidance for using serde -crates: serde +depends-on: serde --- Use `#[derive(Serialize, Deserialize)]` for your types. diff --git a/tests/fixtures/subcommands0/dot-symposium/plugins/example-plugin/SYMPOSIUM.toml b/tests/fixtures/subcommands0/dot-symposium/plugins/example-plugin/SYMPOSIUM.toml index 3be24956..44879a11 100644 --- a/tests/fixtures/subcommands0/dot-symposium/plugins/example-plugin/SYMPOSIUM.toml +++ b/tests/fixtures/subcommands0/dot-symposium/plugins/example-plugin/SYMPOSIUM.toml @@ -1,5 +1,5 @@ name = "example-plugin" -crates = ["*"] +depends-on = ["*"] # A subcommand whose installation runs a known cross-platform binary # (`rustc` is always on PATH inside `cargo test`). The test invokes diff --git a/tests/fixtures/transitive-dep0/dot-symposium/plugins/transitive-plugin/SYMPOSIUM.toml b/tests/fixtures/transitive-dep0/dot-symposium/plugins/transitive-plugin/SYMPOSIUM.toml index 490b6e79..24326a68 100644 --- a/tests/fixtures/transitive-dep0/dot-symposium/plugins/transitive-plugin/SYMPOSIUM.toml +++ b/tests/fixtures/transitive-dep0/dot-symposium/plugins/transitive-plugin/SYMPOSIUM.toml @@ -1,6 +1,6 @@ name = "transitive-plugin" -crates = ["*"] +depends-on = ["*"] [[skills]] -crates = ["mio"] +depends-on = ["mio"] source.path = "skills" diff --git a/tests/fixtures/transitive-dep0/dot-symposium/plugins/transitive-plugin/skills/mio-skill/SKILL.md b/tests/fixtures/transitive-dep0/dot-symposium/plugins/transitive-plugin/skills/mio-skill/SKILL.md index ebbd692d..dd2a54c6 100644 --- a/tests/fixtures/transitive-dep0/dot-symposium/plugins/transitive-plugin/skills/mio-skill/SKILL.md +++ b/tests/fixtures/transitive-dep0/dot-symposium/plugins/transitive-plugin/skills/mio-skill/SKILL.md @@ -1,7 +1,7 @@ --- name: mio-guidance description: Guidance for using mio -crates: mio +depends-on: mio --- This skill should NOT be installed because mio is a transitive dep of tokio. diff --git a/tests/init_sync.rs b/tests/init_sync.rs index 6ca6b026..9f1a7721 100644 --- a/tests/init_sync.rs +++ b/tests/init_sync.rs @@ -385,7 +385,7 @@ async fn add_agent_is_additive() { .unwrap(); } -/// `sync` filters MCP servers by their `crates` predicates. +/// `sync` filters MCP servers by their `depends-on` predicates. #[tokio::test] async fn sync_filters_mcp_servers_by_crates() { with_fixture( @@ -399,12 +399,12 @@ async fn sync_filters_mcp_servers_by_crates() { let settings_path = workspace_root.join(".claude/settings.json"); let settings = std::fs::read_to_string(&settings_path)?; - // always-server (crates = ["*"]) → registered + // always-server (depends-on = ["*"]) → registered assert!( settings.contains("always-server"), "wildcard MCP server should be registered" ); - // serde-server (crates = ["serde"]) → registered (serde is in workspace0) + // serde-server (depends-on = ["serde"]) → registered (serde is in workspace0) assert!( settings.contains("serde-server"), "serde MCP server should be registered" @@ -414,7 +414,7 @@ async fn sync_filters_mcp_servers_by_crates() { settings.contains("inherited-server"), "inherited MCP server should be registered" ); - // missing-crate-server (crates = ["reqwest"]) → NOT registered + // missing-crate-server (depends-on = ["reqwest"]) → NOT registered assert!( !settings.contains("missing-crate-server"), "reqwest MCP server should NOT be registered" @@ -476,7 +476,7 @@ async fn sync_installs_plugin_skill_group() { .unwrap(); } -/// `sync` installs skills from a plugin with `crates = ["*"]`. +/// `sync` installs skills from a plugin with `depends-on = ["*"]`. /// Wildcard predicates should match any workspace. #[tokio::test] async fn sync_installs_wildcard_plugin_skill() { @@ -1894,7 +1894,7 @@ async fn auto_sync_always_runs_on_session_start() { /// Crate metadata redirects: `crate-a` declares a redirect to `crate-b` via /// `[package.metadata.symposium]`. The plugin activates based on plugin-level -/// `crates = ["crate-a"]` and discovers `crate-b`'s skills via redirect. +/// `depends-on = ["crate-a"]` and discovers `crate-b`'s skills via redirect. #[tokio::test] async fn sync_installs_skill_from_named_crate_source() { with_fixture(