diff --git a/.github/workflows/release-pypi.yml b/.github/workflows/release-pypi.yml index ab975ca..db7ed8b 100644 --- a/.github/workflows/release-pypi.yml +++ b/.github/workflows/release-pypi.yml @@ -1,4 +1,4 @@ -# Publish sdist + wheel to PyPI when a SemVer tag is pushed (e.g. v1.1.1). +# Publish sdist + wheel to PyPI when a SemVer tag is pushed (e.g. v1.1.2). # Configure "trusted publishing" on PyPI for this workflow + repository + optional GitHub environment. # https://docs.pypi.org/trusted-publishers/ diff --git a/CHANGELOG.md b/CHANGELOG.md index df62567..d858f32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,16 @@ All notable changes to FlightDeck will be documented in this file. This project follows [Semantic Versioning](https://semver.org/). From **v1.0.0**, documented CLI behavior (**[README.md](https://github.com/flightdeckdev/flightdeck/blob/main/README.md)** on the canonical **`main`** branch), committed **`schemas/v1/`**, and **`POST /v1/events`** payloads with **`api_version` `v1`** are treated as stable public contracts unless a release notes a semver-major bump. -## Unreleased +## 1.1.2 - 2026-05-03 ### Added - **`trace_id` filter** on `GET /v1/runs`, `flightdeck runs list --trace-id`, and SDK `list_runs(trace_id=…)` — exact match on ingested `RunEvent.request.trace_id`. +- **`flightdeck runs export`** — write the same filtered run-event slice as **`runs list`** as **JSONL** (optional **`-o`** file; default stdout); **`--limit`** defaults to **500** (max **500**); prints a **stderr** warning when results are truncated. + +### Changed + +- **Examples / CI snippets:** **`flightdeck-ai>=1.1.2`** where version pins apply. ## 1.1.1 - 2026-05-02 diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 7524de8..191273c 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -133,7 +133,7 @@ Merging to **`main` does not publish packages** — PyPI uploads are **tag-drive 1. **PyPI:** add a **trusted publisher** for **[github.com/flightdeckdev/flightdeck](https://github.com/flightdeckdev/flightdeck)** — workflow **`release-pypi.yml`**. If PyPI offers **Environment name: (Any)**, you can still use a GitHub **Environment** named **`pypi`** for approval gates; otherwise match whatever you register on PyPI ([trusted publishers](https://docs.pypi.org/trusted-publishers/)). 2. **GitHub:** Settings → **Environments** → create **`pypi`** (optional: required reviewers / wait timer before OIDC publish). 3. Bump **`version`** in **`pyproject.toml`** and **`src/flightdeck/__init__.py`**, update **`CHANGELOG.md`**, merge to **`main`**. -4. **`git tag vX.Y.Z`** (must match **`pyproject.toml`** exactly, e.g. **`v1.1.1`**) then **`git push origin vX.Y.Z`**. +4. **`git tag vX.Y.Z`** (must match **`pyproject.toml`** exactly, e.g. **`v1.1.2`**) then **`git push origin vX.Y.Z`**. The workflow runs **ruff**, **pytest**, schema drift, **`uv build`**, publishes **sdist + wheel** to **PyPI** via **OIDC** (no long-lived API token in repo secrets), enables **publish attestations**, and creates a **GitHub Release** with generated notes and **`dist/*`** assets. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index bf5ce59..79da562 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -4,6 +4,10 @@ High-level notes for **shipping FlightDeck**. Detailed history: **[CHANGELOG.md] Narrative docs (including the CLI reference) are maintained on **[github.com/flightdeckdev/flightdeck](https://github.com/flightdeckdev/flightdeck)** `main`; this file and **`schemas/`** ship in minimal clones. +## v1.1.2 — Forensics filters, JSONL export, Phase 1 closure slice + +Patch release (see **[CHANGELOG.md](CHANGELOG.md)**): optional **`trace_id`** filter on **`GET /v1/runs`**, **`flightdeck runs list --trace-id`**, and SDK **`list_runs(trace_id=…)`** (exact match on **`RunEvent.request.trace_id`**); **`flightdeck runs export`** writes the same filtered slice as JSONL (stdout or **`-o`**, **`--limit`** up to **500**, stderr warning when truncated). **Stable contracts:** additive HTTP query param and CLI command only. + ## v1.1.1 — Workspace discovery, web approval UX, docs + CI cookbook Patch release (see **[CHANGELOG.md](CHANGELOG.md)**): **`GET /v1/workspace`** exposes non-secret workspace flags (**`promotion_requires_approval`**, **`pricing_catalog_configured`**, **`server_version`**) for scripting and the **Actions** page; web **Promote** follows the two-step request/confirm path when approval is required; expanded operator docs and **`examples/ci/promote_with_approval.sh`** + README workflow snippet. **Stable contracts:** additive HTTP and JSON Schema only. diff --git a/ROADMAP.md b/ROADMAP.md index f8f0dab..ac37800 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -13,7 +13,7 @@ This roadmap is meant to be clear from **what is already shipped** to **near-ter - **Release registry and verification:** versioned `release.yaml` artifacts with checksums, `flightdeck release verify`. - **Economic + operational governance:** immutable pricing imports, trusted `release diff`, policy-gated `promote` and `rollback`. - **Audit trail:** promotion/rollback history with stable sequencing (`audit_seq`) and integrity checks via `doctor`. -- **Evidence ingestion:** `runs ingest` from JSONL/JSON arrays plus stable `POST /v1/events` contracts (`schemas/v1/`). +- **Evidence ingestion:** `runs ingest` from JSONL/JSON arrays plus stable `POST /v1/events` contracts (`schemas/v1/`); **`GET /v1/runs`**, **`runs list`**, optional **`trace_id`** filter, and **`runs export`** (JSONL) for operator forensics. - **Local API + UI:** `flightdeck serve` routes and web UI (Overview with ledger metrics, Diff, Promote) in `src/flightdeck/server/static/`. - **SDK and tooling:** Python sync/async clients with retries/batching and `flightdeck-quickstart-verify`. @@ -21,7 +21,9 @@ This roadmap is meant to be clear from **what is already shipped** to **near-ter ## Next release -Further **Phase 1** work after **v1.1.1** (deeper forensics / replay UX, OTLP-oriented telemetry per gaps table). Track **[CHANGELOG.md](CHANGELOG.md)**. +Further work after **v1.1.2** — **OTLP-oriented** telemetry, **replay-style web** forensics, deeper **catalog lifecycle** governance, and **cross-workspace / fleet** product surfaces stay on **Phase 2 / mid-term** (see gaps table below). Track **[CHANGELOG.md](CHANGELOG.md)**. + +**v1.1.2** (patch, shipped): **`trace_id`** filter on **`GET /v1/runs`**, **`flightdeck runs list --trace-id`**, and SDK **`list_runs`**, plus **`flightdeck runs export`** (JSONL, stderr warning when truncated); **[examples/README.md](examples/README.md)** adds a **Phase 1 readiness checklist**; **Phase 1 status** (this document) records closure of the scoped productization tranche. See **[CHANGELOG.md](CHANGELOG.md)** and **[RELEASE_NOTES.md](RELEASE_NOTES.md)**. **v1.1.1** (patch, shipped): **`GET /v1/workspace`** (read-only flags + **`server_version`**); web **Actions** page uses it for **direct promote** vs **request / pending list / confirm**; operator docs refresh (**README**, **release-artifact**, **examples**, **web-ui**, **http-api**, **sdk**, **`docs/pricing-catalog.md`**, **SECURITY**); **`examples/ci/promote_with_approval.sh`** and **`github-actions/promote-approval-twostep.yml`**; CI runs a second Playwright pass with **`FD_E2E_FORCE_APPROVAL`**. See **[CHANGELOG.md](CHANGELOG.md)** and **[RELEASE_NOTES.md](RELEASE_NOTES.md)**. @@ -92,13 +94,13 @@ Shipped on **`main`**: Goal: move from solid local tooling to repeatable production usage patterns. -**v1.1.0** ships the first tranche: catalog + hints on diffs, approval-gated promote (HTTP + CLI), read-only runs listing, Helm + fleet reference docs, and migration **v4**. **v1.1.1** closes the **web + discovery** gap for approval (**`GET /v1/workspace`**, Actions UX) and refreshes team docs / CI examples. Remaining bullets below are still in scope for later minors/patches. +**v1.1.0** ships the first tranche: catalog + hints on diffs, approval-gated promote (HTTP + CLI), read-only runs listing, Helm + fleet reference docs, and migration **v4**. **v1.1.1** closes the **web + discovery** gap for approval (**`GET /v1/workspace`**, Actions UX) and refreshes team docs / CI examples. **v1.1.2** adds **CLI/HTTP forensics** filters and **JSONL export**; see **Phase 1 status** below. ### Phase 1 progress (post–v1.1.0 / v1.1.1) - **Approval workflow:** **v1.1.0** added HTTP + CLI + **`GET /v1/promotion-requests`**. **v1.1.1** adds **`GET /v1/workspace`** and a first-class **web** path (request → pending table → confirm) keyed off `promotion_requires_approval`, plus **`examples/ci/promote_with_approval.sh`** and a **`workflow_dispatch`** sample workflow. - **Operator narrative:** README / **examples** index / **web-ui** / **release-artifact** / **http-api** / **sdk** / optional **`docs/pricing-catalog.md`** describe catalog fields, runs listing, and the two promote modes. -- **Still open:** **Forensics** beyond read-only runs list and **`trace_id`-scoped listing** (replay-style views, JSONL exports), richer catalog lifecycle governance, OTLP-oriented telemetry — see gaps table above. +- **Forensics (v1.1.2):** **`trace_id`** filter and **`runs export`** (JSONL). **Still open** for replay-style **web** views and richer export semantics if needed; **OTLP** and **catalog lifecycle** depth remain mid-term (gaps table). ### Build in this phase @@ -115,6 +117,12 @@ Goal: move from solid local tooling to repeatable production usage patterns. - At least two provider pricing sources compare cleanly in one diff workflow. - Teams can stand up and operate `flightdeck serve` with documented deployment guidance. +### Phase 1 status + +**Phase 1 is closed** as of **v1.1.2** for the **scoped local-first productization tranche** shipped across **v1.1.0–v1.1.2**: catalog-aware diffs + hints, approval-gated promote (HTTP + CLI + web), read-only runs forensics (**`GET /v1/runs`**, **`runs list`**, **`trace_id`**, **`runs export`**), reference **Helm** / **Compose** / **fleet** examples, SQLite migration **v4**, **`GET /v1/workspace`**, and a discoverability **checklist** for the Phase-1 **readiness signals** in **[examples/README.md](examples/README.md)**. + +**Carried forward to Phase 2 / mid-term** (explicitly not claimed by this closure): **OTLP-oriented** correlated telemetry; **cross-workspace / fleet** governance products; **replay-style** forensics UI beyond CLI/HTTP listing; **enterprise-grade** identity beyond documented bearer + loopback patterns; deeper **catalog lifecycle** governance (for example version skew hints). + --- ## Phase 2: Scale and platform options (long term, conditional) diff --git a/docs/cli.md b/docs/cli.md index 0a17c2b..6baaa89 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -446,7 +446,7 @@ If no policy has been set, prints the default policy (all constraints `null`/dis ## `flightdeck runs` -Subgroup for ingesting and listing run events. +Subgroup for ingesting, listing, and exporting run events. ### `flightdeck runs list` @@ -458,6 +458,16 @@ flightdeck runs list RELEASE_ID --window WINDOW [--env ENV] [--tenant …] [--ta `--trace-id` filters to events whose ingested `request.trace_id` equals the given string (exact match), same as the `trace_id` query parameter on `GET /v1/runs`. +### `flightdeck runs export` + +Write the same filtered slice as `runs list` (newest first) as **JSONL** — one `RunEvent` JSON object per line. Default **`--limit`** is **500** (maximum **500**). If more events match the window and filters, only the first **`--limit`** lines are written and a **`WARNING:`** line is printed to **stderr** with `exported` / `matching` counts. + +```bash +flightdeck runs export RELEASE_ID --window WINDOW [-o export.jsonl] [--env ENV] [--tenant …] [--task …] [--trace-id ID] [--limit N] +``` + +With **`-o` / `--output`**, writes UTF-8 JSONL to that path; without it, writes to **stdout** (suitable for pipes). + ### `flightdeck runs ingest` Ingest `RunEvent` records from a JSONL or JSON array file. diff --git a/docs/http-api.md b/docs/http-api.md index e00d02b..8754fb6 100644 --- a/docs/http-api.md +++ b/docs/http-api.md @@ -106,7 +106,7 @@ Read-only flags derived from `flightdeck.yaml` plus the running package version. "kind": "WorkspacePublic", "promotion_requires_approval": false, "pricing_catalog_configured": false, - "server_version": "1.1.1" + "server_version": "1.1.2" } ``` diff --git a/examples/README.md b/examples/README.md index 2a142e5..4f76ac1 100644 --- a/examples/README.md +++ b/examples/README.md @@ -8,9 +8,19 @@ This folder holds **copy-pasteable** references for wiring FlightDeck into a rea 2. **Ingest** evidence: `flightdeck runs ingest ` (or JSON array file), or HTTP `POST /v1/events` while `flightdeck serve` is running. 3. **Register** a release bundle: `flightdeck release register ` then **`flightdeck release verify`** against the same tree before you trust the checksum. 4. **Diff and gate** in CI: `flightdeck release diff …` with **`--fail-on-policy`** when you want a non-zero exit without mutating promotion — see [ci/](ci/README.md) and `ledger_gate.py` / GitHub Actions templates. Optional **`pricing_catalog_path`** in `flightdeck.yaml` adds **`pricing.catalog`** / **`pricing.hints`** on diffs (see [docs/pricing-catalog.md](../docs/pricing-catalog.md)). -5. **Promote or rollback** via CLI (`flightdeck release promote` / `rollback`) or HTTP `POST /v1/promote` and `POST /v1/rollback` (token + loopback rules apply). When **`promotion_requires_approval: true`**, use **`release promote-request`** / **`promote-confirm`** or **`POST /v1/promote/request`** then **`POST /v1/promote/confirm`** — see [ci/promote_with_approval.sh](ci/promote_with_approval.sh). +5. **Promote or rollback** via CLI (`flightdeck release promote` / `rollback`) or HTTP `POST /v1/promote` and `POST /v1/rollback` (token + loopback rules apply). When **`promotion_requires_approval: true`**, use **`release promote-request`** / **`promote-confirm`** or **`POST /v1/promote/request`** then **`POST /v1/promote/confirm`** — see [ci/promote_with_approval.sh](ci/promote_with_approval.sh) and [ci/README.md](ci/README.md) (GitHub Actions patterns). 6. **Run the server** in a container or compose stack — see [deploy/](deploy/README.md). The bundled UI calls **`GET /v1/workspace`** to choose direct promote vs request/confirm. -7. **Triage runs** with **`flightdeck runs list`** or **`GET /v1/runs`**, and **observe** aggregate ledger size with **`GET /v1/metrics`** (JSON counters; read-only, same access tier as other `GET /v1/*` routes). +7. **Triage runs** with **`flightdeck runs list`** / **`runs export`** or **`GET /v1/runs`**, and **observe** aggregate ledger size with **`GET /v1/metrics`** (JSON counters; read-only, same access tier as other `GET /v1/*` routes). + +## Phase 1 readiness (quick checklist) + +Use this as a **discoverability** pass for the **[ROADMAP.md](../ROADMAP.md)** Phase-1 readiness signals (not a product guarantee): + +| Signal | Where to start | +|--------|----------------| +| **Approval-gated promote in CI** | [ci/promote_with_approval.sh](ci/promote_with_approval.sh), [ci/README.md](ci/README.md), [ci/github-actions/promote-approval-twostep.yml](ci/github-actions/promote-approval-twostep.yml) | +| **Two-provider (or catalog) pricing on a diff** | [docs/pricing-catalog.md](../docs/pricing-catalog.md); tests **`test_diff_cross_provider_releases`** and **`test_catalog_comparable_cost_on_cross_provider_diff`** in `tests/` | +| **Operate `flightdeck serve` with deployment guidance** | [deploy/README.md](deploy/README.md) (**Compose healthcheck**, **`FLIGHTDECK_LOCAL_API_TOKEN`**, **SQLite backup** via **`flightdeck doctor --backup`**); optional **Helm** under [deploy/chart/flightdeck/](deploy/chart/flightdeck/) | ## Subfolders diff --git a/examples/ci/README.md b/examples/ci/README.md index 733a865..2fe7e31 100644 --- a/examples/ci/README.md +++ b/examples/ci/README.md @@ -37,7 +37,7 @@ uv run python examples/ci/ledger_gate.py Example (**PyPI** install): ```bash -pip install "flightdeck-ai>=1.1.1" +pip install "flightdeck-ai>=1.1.2" export WORKSPACE="$(mktemp -d)" export QUICKSTART_ROOT=/path/to/flightdeck/examples/quickstart python /path/to/flightdeck/examples/ci/ledger_gate.py diff --git a/examples/ci/github-actions/policy-gate-pypi.yml b/examples/ci/github-actions/policy-gate-pypi.yml index d8aa794..dde10b2 100644 --- a/examples/ci/github-actions/policy-gate-pypi.yml +++ b/examples/ci/github-actions/policy-gate-pypi.yml @@ -11,7 +11,7 @@ on: env: # Pin to a tag or SHA that matches your installed flightdeck-ai version when possible. FLIGHTDECK_REF: main - FLIGHTDECK_AI_SPEC: ">=1.1.1" + FLIGHTDECK_AI_SPEC: ">=1.1.2" jobs: ledger-gate: diff --git a/examples/deploy/Dockerfile b/examples/deploy/Dockerfile index a2788eb..823e9b7 100644 --- a/examples/deploy/Dockerfile +++ b/examples/deploy/Dockerfile @@ -2,7 +2,7 @@ FROM python:3.14-slim RUN pip install --no-cache-dir --upgrade pip \ - && pip install --no-cache-dir "flightdeck-ai>=1.1.1" + && pip install --no-cache-dir "flightdeck-ai>=1.1.2" WORKDIR /workspace diff --git a/examples/deploy/README.md b/examples/deploy/README.md index 625cbf8..2c68c30 100644 --- a/examples/deploy/README.md +++ b/examples/deploy/README.md @@ -69,4 +69,5 @@ Compose sets a **`healthcheck`** on **`/health`** plus restart policies; for sys ## Related - **[examples/integration/README.md](../integration/README.md)** — emit `RunEvent` traffic into a running server. -- **[examples/ci/README.md](../ci/README.md)** — CI policy gates without `serve`. +- **[examples/ci/README.md](../ci/README.md)** — CI policy gates without `serve`; **approval-gated promote** script [promote_with_approval.sh](../ci/promote_with_approval.sh) and workflow samples. +- **[SECURITY.md](../../SECURITY.md)** — trust boundaries before exposing **`/v1/*`** beyond loopback. diff --git a/examples/deploy/chart/flightdeck/Chart.yaml b/examples/deploy/chart/flightdeck/Chart.yaml index dcffa04..b6e9a2e 100644 --- a/examples/deploy/chart/flightdeck/Chart.yaml +++ b/examples/deploy/chart/flightdeck/Chart.yaml @@ -3,4 +3,4 @@ name: flightdeck description: Optional Helm chart for flightdeck serve (single-replica reference) type: application version: 0.1.0 -appVersion: "1.1.1" +appVersion: "1.1.2" diff --git a/pyproject.toml b/pyproject.toml index 7cc2b06..4a9acb4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "flightdeck-ai" -version = "1.1.1" +version = "1.1.2" description = "Ship AI agents safely with release diffs, runtime evidence, and policy gates." readme = "README.md" license = "Apache-2.0" diff --git a/src/flightdeck/__init__.py b/src/flightdeck/__init__.py index 26e2ccc..c91200d 100644 --- a/src/flightdeck/__init__.py +++ b/src/flightdeck/__init__.py @@ -1,3 +1,3 @@ """FlightDeck — ship AI agents safely with release diffs, runtime evidence, and policy gates.""" -__version__ = "1.1.1" +__version__ = "1.1.2" diff --git a/src/flightdeck/cli/main.py b/src/flightdeck/cli/main.py index 87ab906..5f8924e 100644 --- a/src/flightdeck/cli/main.py +++ b/src/flightdeck/cli/main.py @@ -4,6 +4,7 @@ import json import os +import sys from pathlib import Path from uuid import uuid4 @@ -312,7 +313,7 @@ def policy_show() -> None: @cli.group() def runs() -> None: - """Ingest run events.""" + """Ingest, list, and export run events.""" @runs.command("list") @@ -369,6 +370,66 @@ def runs_list( click.echo(json.dumps(ev, sort_keys=True)) +@runs.command("export") +@click.argument("release_id") +@click.option("--window", required=True, help="Time window like 7d, 24h, 30m.") +@click.option("--env", "environment", default=None) +@click.option("--tenant", "tenant_id", default=None) +@click.option("--task", "task_id", default=None) +@click.option("--trace-id", "trace_id", default=None, help="Filter to events whose request.trace_id matches (exact).") +@click.option("--limit", default=500, show_default=True, type=click.IntRange(1, 500)) +@click.option( + "-o", + "--output", + "output_path", + type=click.Path(path_type=Path), + default=None, + help="Write JSONL to this file; default is stdout.", +) +def runs_export( + release_id: str, + window: str, + environment: str | None, + tenant_id: str | None, + task_id: str | None, + trace_id: str | None, + limit: int, + output_path: Path | None, +) -> None: + """Export run events as JSONL (newest first), same filters as ``runs list`` (truncated to --limit, max 500).""" + cfg = load_config() + storage = Storage(cfg.db_path) + storage.migrate() + try: + payload = query_run_events_page( + cfg=cfg, + storage=storage, + release_id=release_id, + window=window, + environment=environment, + tenant_id=tenant_id, + task_id=task_id, + trace_id=trace_id, + limit=limit, + ) + except OperationError as e: + raise click.ClickException(str(e)) from e + stream = output_path.open("w", encoding="utf-8", newline="\n") if output_path else None + try: + out = stream or sys.stdout + for ev in payload["events"]: + out.write(json.dumps(ev, sort_keys=True) + "\n") + finally: + if stream is not None: + stream.close() + if payload["truncated"]: + click.echo( + f"WARNING: exported {payload['returned']} of {payload['matched_total']} matching events " + f"(cap --limit {limit}).", + err=True, + ) + + @runs.command("ingest") @click.argument("path", type=click.Path(exists=True, path_type=Path)) def runs_ingest(path: Path) -> None: diff --git a/tests/test_phase1_features.py b/tests/test_phase1_features.py index 9b0351a..70c0c49 100644 --- a/tests/test_phase1_features.py +++ b/tests/test_phase1_features.py @@ -301,6 +301,30 @@ def test_cli_runs_list_json(tmp_path: Path, monkeypatch) -> None: assert payload["matched_total"] == 1 +def test_cli_runs_export_jsonl_truncation_and_stderr(tmp_path: Path, monkeypatch) -> None: + monkeypatch.chdir(tmp_path) + runner = CliRunner() + assert runner.invoke(cli, ["init"]).exit_code == 0 + policy = write_policy(tmp_path, min_candidate_runs=0, min_baseline_runs=0, min_low_runs=0) + assert runner.invoke(cli, ["policy", "set", str(policy)]).exit_code == 0 + pricing = write_pricing(tmp_path, provider="openai", pricing_version="openai-2026-04-30") + assert runner.invoke(cli, ["pricing", "import", str(pricing)]).exit_code == 0 + rdir = write_release(tmp_path, agent_id="ag", version="1", pricing_provider="openai", pricing_version="openai-2026-04-30") + rid = runner.invoke(cli, ["release", "register", str(rdir)]).output.strip() + now = datetime.now(tz=timezone.utc) + assert runner.invoke(cli, ["runs", "ingest", str(write_events(tmp_path, release_id=rid, agent_id="ag", n=5, ts=now))]).exit_code == 0 + out_path = tmp_path / "exp.jsonl" + res = runner.invoke( + cli, + ["runs", "export", rid, "--window", "7d", "--limit", "2", "-o", str(out_path)], + ) + assert res.exit_code == 0 + lines = [ln for ln in out_path.read_text(encoding="utf-8").splitlines() if ln.strip()] + assert len(lines) == 2 + assert "WARNING" in (res.stderr or "") + assert "exported 2 of 5" in (res.stderr or "") + + def test_diff_survives_malformed_catalog_yaml_syntax(tmp_path: Path, monkeypatch) -> None: """Invalid YAML in pricing catalog must not crash diff (YAMLError → catalog warning).""" monkeypatch.chdir(tmp_path) diff --git a/uv.lock b/uv.lock index 7e98277..1ad8c4a 100644 --- a/uv.lock +++ b/uv.lock @@ -167,7 +167,7 @@ wheels = [ [[package]] name = "flightdeck-ai" -version = "1.1.1" +version = "1.1.2" source = { editable = "." } dependencies = [ { name = "aiosqlite" },