Skip to content

Add custom predicate extensions via [[predicate]]#238

Merged
nikomatsakis merged 13 commits into
symposium-dev:mainfrom
nikomatsakis:custom-predicates
Jun 9, 2026
Merged

Add custom predicate extensions via [[predicate]]#238
nikomatsakis merged 13 commits into
symposium-dev:mainfrom
nikomatsakis:custom-predicates

Conversation

@nikomatsakis

Copy link
Copy Markdown
Member

What does this PR do?

Plugin authors can now define custom predicate functions that extend the predicate language. When a crates expression uses an unknown function name (e.g., battery-pack(cli>=0.3)), Symposium looks it up in the custom predicate registry and spawns the declared command with the raw argument appended.

Key additions:

  • Predicate::Custom variant with balanced-paren parsing
  • [[predicate]] section in plugin TOML manifests
  • CustomPredicateEvaluator with per-sync caching
  • Witness support (stdout JSON with selectedCrates)
  • Runnable::spawn helper on symposium-install
  • Full sync integration (predicates resolved before evaluation)
  • Integration tests and reference documentation
Disclosure questions

AI disclosure.

  • The AI tool authored large parts of the code

Questions for reviewers.

Comment thread md/reference/plugin-definition.md Outdated
Comment thread src/plugins.rs Outdated
Comment thread src/predicate.rs Outdated
Comment thread src/predicate.rs
Comment thread src/predicate.rs Outdated
Comment thread src/skills.rs Outdated
Plugin authors can now define custom predicate functions that extend
the predicate language. When a crates expression uses an unknown
function name (e.g., `battery-pack(cli>=0.3)`), Symposium looks it up
in the custom predicate registry and spawns the declared command with
the raw argument appended.

Key additions:
- Predicate::Custom variant with balanced-paren parsing
- [[predicate]] section in plugin TOML manifests
- CustomPredicateEvaluator with per-sync caching
- Witness support (stdout JSON with selectedCrates)
- Runnable::spawn helper on symposium-install
- Full sync integration (predicates resolved before evaluation)
- Integration tests and reference documentation

Co-authored-by: Claude <claude@anthropic.com>
nikomatsakis and others added 5 commits June 9, 2026 15:17
Move the CustomPredicateEvaluator's entries and cache directly into
PredicateContext, making evaluate()/witness() take &mut self. This
eliminates the separate _with_evaluator function variants and fixes a
bug where group-level custom predicates were silently evaluated as
false (because load_skills_for_group called evaluate() without the
external evaluator).

Co-authored-by: Claude <claude@anthropic.com>
- Extract `validate_custom_predicate_name` as a single public helper in
  predicate.rs, enforcing `[a-zA-Z][a-zA-Z0-9_]*` + no builtin collision.
  Used by both the expression parser and plugin manifest validation.
- Fix CrateList::parse to use split_top_level (paren-aware comma split)
  so custom predicates like `bp(a, b)` in SKILL.md frontmatter are not
  incorrectly split on the inner comma.
- Add collision tests for build_custom_predicate_registry (two-way and
  three-way).

Co-authored-by: Claude <claude@anthropic.com>
The `crates` field should only accept crate-name atoms (e.g. `serde`,
`serde>=1.0`, `*`). Full predicate expressions including custom
predicates belong in the `predicates` field. This matches the design
decision from the Zulip discussion: the fields serve distinct roles
and should not accept each other's syntax.

- AtomParser now errors on `(` instead of parsing function calls
- Remove dead `consume_balanced_parens` from AtomParser
- Fix test fixture to use `predicates` instead of `crates`
- Update CrateList tests to assert rejection

Co-authored-by: Claude <claude@anthropic.com>
Assert that the `crates` field rejects all function-call forms (all(),
crate(), not(), shell()) and that the `predicates` field rejects bare
crate-atom syntax (serde, tokio>=1.0, *).

Co-authored-by: Claude <claude@anthropic.com>
The custom predicate tests used Runnable::Exec with hardcoded
/bin/true and /bin/false paths which may not exist on all CI
environments. Replace with Runnable::Script using tempfile shell
scripts that explicitly `exit 0` or `exit 1`.

Co-authored-by: Claude <claude@anthropic.com>
Comment thread tests/custom_predicates.rs
Comment thread tests/custom_predicates.rs
Comment thread tests/custom_predicates.rs
Comment thread tests/custom_predicates.rs
…ource

Address PR review feedback:
- Add comment to fixture explaining the script is test-generated
- Add test that verifies the predicate argument ("cli") is passed correctly
- Add test showing wrong argument causes predicate failure
- Add witness integration test: custom predicate returns selectedCrates
  JSON, which drives `source = "crate"` skill resolution from a path dep

Co-authored-by: Claude <claude@anthropic.com>
Comment thread md/reference/plugin-definition.md Outdated
nikomatsakis and others added 2 commits June 9, 2026 17:55
Previously, invalid JSON on stdout was silently ignored (predicate
still passed with no witness). Now non-empty invalid stdout causes the
predicate to evaluate as false, since the command likely has a bug.
Empty stdout remains valid (pass with no witness crates).

Co-authored-by: Claude <claude@anthropic.com>
Any invalid entry in selectedCrates (e.g. bad semver) now fails the
whole predicate rather than silently skipping it. Consistent with the
principle that non-empty invalid stdout means the command has a bug.

Co-authored-by: Claude <claude@anthropic.com>
Comment thread md/reference/plugin-definition.md
- `foo()` and `foo( )` do not append any argument to the command
- Leading/trailing whitespace is stripped from the argument
- Add unit tests verifying parsing and runtime behavior

Co-authored-by: Claude <claude@anthropic.com>
@nikomatsakis nikomatsakis marked this pull request as ready for review June 9, 2026 18:04
nikomatsakis and others added 3 commits June 9, 2026 20:58
…ource

- Add cross-plugin predicate tests: one plugin defines [[predicate]],
  another plugin uses it in predicates = [...]. Demonstrates that
  custom predicates are always registered regardless of which plugin
  defines them and can be used by any other plugin.
- Tests both pass and fail paths for cross-plugin usage.

Co-authored-by: Claude <claude@anthropic.com>
Replace the raw HashMap<String, ResolvedCustomPredicate> with a
dedicated CustomPredicateRegistry struct that exposes only the
operations we need (get, iter, contains_key, len, is_empty).

Co-authored-by: Claude <claude@anthropic.com>
@nikomatsakis nikomatsakis added this pull request to the merge queue Jun 9, 2026
Merged via the queue into symposium-dev:main with commit 0e56c46 Jun 9, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant