Changelog¶
All notable changes to PromptCanary are documented here.
This file follows Keep a Changelog and this project adheres to Semantic Versioning.
Unreleased¶
Planned¶
- Async parallel prompt execution (
suite.arun()) - Export connectors: Langfuse, Phoenix/Arize
SemanticSimilarityProbe— embedding-based semantic drift probe (optional dep)- S3/GCS baseline storage backend
- Optional web dashboard
0.2.3 — 2026-07-04¶
Fixed¶
notebooks/custom_probes.ipynbfailed to open in Google Colab withSyntaxError: Expected double-quoted property name in JSON at position 15924. Root cause: a stray\n",fragment — left over from an earlier manual edit — immediately after thekernelspecobject in the notebook's metadata block, which made the file invalid JSON. The file happened to still open in some tools despite this, which is how it shipped unnoticed; Colab's stricter parser caught it correctly. Fixed by removing the six-character artifact.- All four notebooks were also missing per-cell
idfields, required as of nbformat 4.5 (which every notebook in this repo declares vianbformat_minor: 5) and flagged bynbformat.validate()asMissingIDFieldWarning, "will become a hard error in future nbformat versions." Fixed by round-tripping every notebook throughnbformat's reader/writer, which transparently backfills missing ids.
Added¶
scripts/validate_notebooks.py— validates every notebook undernotebooks/as well-formed JSON, schema-valid nbformat, and free of duplicate cell ids. Written directly in response to the incident above: this exact class of corruption (valid-looking file, invalid JSON) is precisely what this script checks for before a notebook ships, rather than relying on it being caught by whichever tool a user happens to open it with next..github/workflows/ci.yml— newnotebooksjob runs the validator on every push and PR;buildnow depends on it passing, so a corrupted notebook blocks a release the same way a failing test would.CONTRIBUTING.md— PR checklist now includes running the notebook validator for any PR that adds or hand-edits a.ipynbfile.
0.2.2 — 2026-07-04¶
Fixed¶
Another CI-only mypy failure, caught the same way as v0.2.1's fixes: by installing strictly from pyproject.toml into a fresh virtualenv rather than trusting an accumulated local dev environment
promptcanary/core/suite.py:190—mypyreportedReturning Any from function declared to return "str"onCanarySuite.to_yaml_template()'s call toyaml.dump(...). Root cause:types-PyYAMLwas present in the local development environment from earlier ad hoc installs but was never declared inpyproject.toml's[dev]extras, so a cleanpip install -e ".[dev]"(exactly what CI runs) leftyaml.dump()untyped (Any), which mypy strict mode correctly flagged against the function's declared-> strreturn type. Fixed by addingtypes-PyYAMLto[dev].pyproject.toml[project.optional-dependencies.viz]— removedpandas, which was declared but never actually imported or used anywhere in the codebase (only mentioned in a docstring). Its sole effect was pulling innumpyas a transitive dependency;numpy2.5 ships type stubs using unconditional Python 3.12-only syntax (PEP 695type X = ...statements), which fails to parse under this project's deliberatepython_version = "3.10"mypy target — a hard stub-parse error thatignore_missing_imports, per-moduleignore_errors, andfollow_imports = "skip"overrides all failed to work around, sincepandas's own stubs importnumpyinternally regardless of override targeting. Removing the unused dependency fixes the root cause rather than working around the symptom. SeeDECISION_LOG.mdADR-011 for the full investigation and rejected alternatives.
Process¶
- Established clean-room verification as standard practice before any release: install only what
pyproject.tomldeclares into a fresh, empty virtualenv (never trust a long-lived local dev environment that has accumulated packages from earlier work) and run the fullruff check→ruff format --check→mypy→pytestloop, for every meaningful combination of optional extras ([dev]alone, matching CI exactly;[dev,viz]together, matching a full local contributor setup). Both this fix and v0.2.1'shypothesis/typer[all]fixes were found by this exact process — documented here so it isn't lost as tribal knowledge.
Changed¶
- Fixed placeholder GitHub org throughout the project. Every badge, URL, and cross-reference previously pointed to a placeholder
promptcanary/promptcanaryorg (32 occurrences acrossREADME.md,CONTRIBUTING.md,CHANGELOG.md,pyproject.toml,mkdocs.yml, CI/issue templates, docstrings, and non-mirrored docs pages) — corrected to the real repository,Mattral/PromptCanary. - Fixed a related inconsistency: the
DocumentationURL inpyproject.tomlandsite_urlinmkdocs.ymlpointed to a ReadTheDocs domain that was never set up — the actual deployment target (per.github/workflows/docs.yml'smkdocs gh-deploy) is GitHub Pages. Both now correctly point tohttps://mattral.github.io/PromptCanary/. README.mdbadge row replaced with a fuller set (CI, PyPI, Python version, License, Ruff, mypy, a Hits view counter, and a Colab notebooks launch badge), all pointing to the real repo.notebooks/README.mdadded — direct "Open in Colab" launch links for all four notebooks (quickstart,custom_probes,analyzing_drift_trends,ci_integration), so notebooks are runnable with zero local setup.
0.2.1 — 2026-07-03¶
Fixed¶
CI pipeline (all four checks now pass cleanly: ruff check, ruff format, mypy strict, pytest)
- pyproject.toml — added hypothesis, typer[all] to [dev] extras; these were required by the test suite (tests/unit/test_property_based.py, tests/unit/test_cli.py) but missing from the dependency declaration, causing ModuleNotFoundError in CI
- .github/workflows/ci.yml — the lint job installed a hand-rolled dependency list instead of pip install -e ".[dev]"; this duplicated pyproject.toml's dependencies by hand and had already drifted out of sync (missing hypothesis, PyYAML version pin, etc.). Now uses the single source of truth. Also extended lint/format checks to cover tests/, not just promptcanary/.
- pyproject.toml — removed redundant typer[all] from [dev] extras (triggered a deprecation warning on install: newer typer releases folded the [all] extra's contents — rich, shellingham — into core dependencies, and rich was already declared separately in the base dependencies list)
- 35 files reformatted with ruff format — the formatter had never actually been run against the codebase; ruff format --check was failing on 22 of 35 source/test files despite ruff check (linting) passing
- 15 mypy strict-mode errors resolved across 7 files:
- Removed 8 redundant # type: ignore[import-untyped] comments — pyproject.toml already sets ignore_missing_imports = true project-wide, making per-line ignores dead code that mypy strict mode correctly flags as unused
- core/suite.py — _NoopContext (the progress-bar no-op fallback) now implements add_task()/update() matching Rich's Progress API, returning a proper TaskID; this removes two union-attr errors and lets the if show_progress: guards around progress calls be deleted entirely, since both branches now share an interface
- core/probes/tool_use.py — _find_args() now narrows through a local variable before returning, fixing a no-any-return error where obj[k] on an Any-typed dict couldn't be verified as dict[str, Any] despite the preceding isinstance check
- core/probes/reasoning.py — VerbosityProbe.evaluate()'s meta dict is now explicitly typed dict[str, Any]; it holds both int (word counts) and float (ratio) values across branches, which mypy's inferred dict[str, int] from the first assignment couldn't accommodate
- cli.py — the report command's format-detection branch (DriftReport vs CanaryRunResult) now declares explicit obj: CanaryRunResult | DriftReport and reporter_obj: Reporter | DriftReporter union types, since the two branches assign genuinely different, unrelated types to the same variable names
Test coverage
- promptcanary/providers/litellm.py: 21% → 100% coverage. Added tests/unit/test_litellm_provider.py (29 tests) covering LiteLLMProvider.complete()'s full request-building and response-parsing logic — message construction (system prompt included/excluded), parameter forwarding (temperature, max_tokens, seed, extra_params), token usage extraction, raw_response capture, and all three error paths (missing litellm package, API call failure with status-code preservation, malformed response structure). Mocks litellm.completion() directly per ADR-008 — zero real network calls.
- Overall coverage: 89% → 92% (264 tests, up from 235)
Added¶
CODE_OF_CONDUCT.md— full Contributor Covenant v2.1 text; previously referenced byCONTRIBUTING.mdbut the file itself didn't exist.github/ISSUE_TEMPLATE/bug_report.yml,.github/ISSUE_TEMPLATE/feature_request.yml,.github/ISSUE_TEMPLATE/config.yml— structured issue forms, blank issues disabled, links to Discussions and the security policy.github/PULL_REQUEST_TEMPLATE.md— PR checklist matchingCONTRIBUTING.md's stated requirementsdocs/integrations/llamaindex.md— LlamaIndex integration guide (direct-LLM and query-engine-wrapping patterns, retrieval-drift-specific custom probe example); explicitly named in the original project guideline's integration list but was missingdocs/sync_root_docs.py— now rewrites cross-references between mirrored root docs (e.g. a link toCODE_OF_CONDUCT.mdinsideCONTRIBUTING.mdresolves tocode-of-conduct.mdin the mirrored copy) somkdocs build --strictstays clean without touching the root source files' correct GitHub-relative linksexamples/multi_provider.py— runs the same suite across OpenAI, Anthropic, and Ollama simultaneously (with a--mockflag for no-API-key exploration), with a cross-provider comparison table and an interpretation guide for distinguishing provider-specific drift from harness-level regressionsexamples/custom_probe_example.py— three complete custom-probe patterns (@probedecorator,BaseProbesubclass, and a full production-grade JSON-API-contract probe with partial scoring) runnable end-to-end with no API key
Changed¶
docs.yml— addedCODE_OF_CONDUCT.mdto the docs-rebuild trigger pathsmkdocs.yml— added Code of Conduct and LlamaIndex pages to nav
0.2.0 — 2026-06-30¶
Added¶
Tool Use probes (new category, 4 probes)
- ToolCallPresenceProbe — detects presence/absence of any function call, with auto/json/text detection strategies
- ToolCallNameProbe — verifies the correct function is called, with alias support and partial credit (0.3) when the wrong function is called vs. none at all
- ToolCallArgsProbe — required/forbidden argument key validation, handles OpenAI's JSON-encoded arguments string format
- ToolCallSchemaProbe — full structural validation combining name (40%) + args (40%) + arg types (20%) into one weighted score
Trend visualization (promptcanary.utils.visualization)
- plot_score_history() — overall score and pass rate over time
- plot_probe_heatmap() — per-probe score grid across snapshots, reveals which probe regresses first
- plot_drift_timeline() — regression count and severity over a series of compare() calls
- Zero-dependency ASCII fallback (sparklines, tables) always available; optional Plotly HTML rendering via pip install promptcanary[viz]
- Graceful degradation: mode="auto" tries Plotly, falls back to ASCII with no functionality lost
Property-based testing (Hypothesis)
- Score invariants: all probe scores remain in [0.0, 1.0] under arbitrary input
- CanaryRunResult.overall_score proven to equal the mean of all probe scores
- DriftReport.overall_score_delta proven to equal current − baseline exactly
- Identical-run comparisons proven to never produce false-positive regressions
- JsonValidityProbe proven binary (never partial) across arbitrary text and arbitrary valid JSON
- FileBaselineStore round-trip (save → load) proven lossless for suite name, scores, and pass/fail state
- Regression and improvement flags proven mutually exclusive
CLI test coverage
- Full typer.testing.CliRunner coverage for init, run, compare, baselines, report, version
- Stub provider pattern for testing without real network calls
Documentation site (MkDocs Material)
- 23-page documentation site: getting started, core concepts, full probe reference, per-provider guides, CI/CD guides, integration guides (LangChain, FastAPI), visualization guide, CLI reference, auto-generated API reference via mkdocstrings
- docs/sync_root_docs.py — keeps docs/decision-log.md, docs/contributing.md, docs/changelog.md in sync with their root-level sources of truth
- .github/workflows/docs.yml — auto-deploys to GitHub Pages on changes to docs or docstrings
- Strict-mode build verified (zero broken links, zero missing nav references)
Notebooks
- notebooks/analyzing_drift_trends.ipynb — simulates 7 days of gradual provider drift, identifies the first-failing probe, demonstrates all three visualization types
- notebooks/ci_integration.ipynb — GitHub Actions patterns, multi-provider matrix, cost-aware scheduling strategy, baseline promotion workflow, current provider reference table
Changed¶
- Updated all model references to current generation (per community feedback):
openai/gpt-5.4/gpt-5.5replacegpt-4o;anthropic/claude-sonnet-4-6/claude-opus-4-8replaceclaude-3-5-sonnet-20241022;gemini/gemini-3.5-flash/gemini-3.1-proreplacegemini-1.5-pro;xai/grok-4replacesgrok-beta. Applied across README, CLI help text,examples/, notebooks, and GitHub Actions workflows. - README — Supported Providers section rewritten with a comparison table covering cost tier and API key requirements per provider, explicit free/local Ollama models (
qwen3.6:27b,deepseek-r1:14b,gpt-oss:20b) presented as first-class options rather than an afterthought, and a rationale section on why free local models are valuable as zero-cost canaries - README — Python SDK section now shows Gemini and Ollama usage alongside OpenAI, not just a single-provider example
- Probe count updated throughout docs: 15 → 19 (addition of the Tool Use category)
Fixed¶
@probedecorator now correctly satisfies theBaseProbeABC contract (previously relied on a post-hoc method assignment that worked but was fragile underabc.ABCMeta's abstract-method check in edge cases); rewritten usingtype()to construct the class withevaluatealready boundToolCallArgsProbe._extract_arg_keys()now correctly parses OpenAI'stool_calls[].function.argumentsformat, whereargumentsis a JSON-encoded string rather than a nested object
0.1.0 — 2026-06-29¶
Added¶
Core pipeline
- CanarySuite — orchestrates prompts, probes, and provider calls with progress display
- CanarySuite.from_yaml() — load a full suite from a canary.yaml config file
- CanarySuite.to_yaml_template() — serialise back to YAML for inspection
- compare() — compare a CanaryRunResult against a BaselineSnapshot to produce a DriftReport
- FileBaselineStore — local JSON baseline storage with save, load, load_latest, list, delete
Pydantic v2 models (fully typed, frozen where appropriate)
- CanaryPrompt — prompt with id, text, tags, expected_keywords, system_prompt override
- ProviderConfig — provider + temperature + seed + extra_params
- LLMResponse — raw provider response with token counts, latency, finish_reason
- ProbeResult — structured result with score, pass/fail, details, metadata
- CanaryRunResult — aggregated run with derived properties (overall_score, pass_rate, by_category)
- BaselineSnapshot — versioned snapshot with schema_version field
- ProbeComparison — side-by-side comparison for one probe × one prompt
- DriftReport — full drift analysis with severity heuristic and human-readable summary
Built-in probes (15 probes across 4 categories)
Format & Structure
- JsonValidityProbe — is the response valid JSON?
- JsonSchemaProbe — required/forbidden JSON keys, partial scoring
- JsonKeyOrderProbe — LCS-based key order comparison
- ResponseLengthProbe — char-count bounds + drift scoring vs baseline
- MarkdownHeaderProbe — expected markdown section headers
- KeywordPresenceProbe — required/forbidden keyword matching
- ExpectedKeywordsProbe — uses keywords declared on CanaryPrompt
Reasoning Style
- StepByStepProbe — detects step-by-step reasoning signals
- VerbosityProbe — word-count drift with tolerance band
- ConfidenceLanguageProbe — hedging vs. confident language heuristic
- DirectAnswerProbe — preamble detection ("Sure!", "Great question!", "As an AI…")
Safety & Refusal
- RefusalProbe — detects LLM refusals vs compliance
- SafetyLanguageProbe — detects disclaimer/caveat injection
Factual
- FactualConsistencyProbe — exact/contains/startswith match against known value
- SentimentProbe — lightweight keyword-based tone probe
Probe extension API
- BaseProbe ABC — evaluate(prompt, response) → ProbeResult
- @probe decorator — turn any function into a registered, nameable probe
- Global probe registry with get_probe() and get_probe_registry()
Provider layer
- BaseLLMProvider ABC — implement complete() for any backend
- LiteLLMProvider — unified adapter for OpenAI, Anthropic, Google, Ollama, vLLM, and 100+ more
- ProviderError — normalised error type with model_id, status_code, raw_error
CLI (promptcanary)
- promptcanary init <name> — scaffold suite directory with canary.yaml, .env.example, README.md
- promptcanary run — run suite, print Rich terminal report, optionally save baseline and emit JSON/MD/HTML
- promptcanary compare — compare to saved baseline, detect drift, CI exit code support
- promptcanary baselines — list saved baselines in a rich table
- promptcanary report — offline report generation from saved JSON
- promptcanary version — print installed version
Reporting
- Terminal (Rich): colour-coded table, per-category scores, duration, run ID
- Markdown: GitHub-flavoured, PR-comment-ready, per-probe table + category stats
- HTML: self-contained dark-theme report with score bars
- JSON: fully serialisable via Pydantic model_dump(mode="json")
Drift reporting - Terminal: severity-coloured panel, regressions table with delta - Markdown: structured with metrics table, regressions, improvements sections - HTML: interactive comparison table - Severity heuristic: NONE / LOW / MEDIUM / HIGH / CRITICAL based on regression rate and delta
GitHub Actions
- ci.yml — lint (ruff), type-check (mypy), test matrix (Python 3.10/3.11/3.12 × ubuntu/macos/windows), build + PyPI release via Trusted Publishing
- promptcanary.yml — weekly scheduled drift check, PR comment integration, GitHub issue on drift, artifact upload
Testing
- tests/conftest.py — shared fixtures: MockLLMProvider, canonical prompts/responses, temp dirs
- tests/unit/test_models.py — comprehensive Pydantic model tests
- tests/unit/probes/test_format_probes.py — all format probes, happy + failure + edge cases
- tests/unit/probes/test_reasoning_safety_probes.py — reasoning and safety probe coverage
- tests/unit/test_suite_comparator_storage.py — suite, comparator, FileBaselineStore, Reporter
- tests/integration/test_full_pipeline.py — end-to-end run→baseline→compare workflow
Packaging
- pyproject.toml with Hatchling build backend, optional extras [viz], [dev], [all]
- py.typed marker (PEP 561)
- promptcanary console script entry point
- ruff + mypy strict configuration
Documentation & examples
- README.md — full DX-first documentation with quickstart, API reference, CI setup
- CHANGELOG.md — this file
- DECISION_LOG.md — architecture decision records
- CONTRIBUTING.md — contribution guide with commit conventions
- examples/canary.yaml — production-grade example suite (8 prompts, 7 probes)
- examples/quickstart.py — standalone runnable example
Versioning Policy¶
- MAJOR (1.x.x): breaking changes to the public API (models, probe interface, CLI)
- MINOR (0.x.0): new features, new probes, new providers — backward compatible
- PATCH (0.1.x): bug fixes, documentation improvements, test additions