Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 18 additions & 20 deletions HANDOFF.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,36 @@

## Finished

- Scanned repository structure, duplication, import boundaries, error handling, and test health; recorded scope and remaining priorities in `docs/quality-review-2026-09-30.md`.
- Isolated plugin commands so help and catalog search avoid bootstrap, deployment, and provider modules.
- Shared the existing durable-run identity rule between S3 and PostgreSQL, with storage behavior preserved.
- Updated four dependencies after protected CI identified 20 published advisories; the strict runtime dependency audit is now clean locally.
- Refreshed flagged system packages in runtime/starter, Spark, and OCI Dockerfiles; the runtime OS layer and complete Spark/OCI images pass local scans.
- Added a read-only comparison of an unsaved graph with its exact saved revision, including downstream outputs and write effects.
- Added readable explanations of recorded run outcomes and eligible next actions; unavailable measurements remain unknown.
- Exposed both operations through existing Control authentication and regenerated their public contracts.

## Try It

Run `uv run dander plugins --help` and `uv run dander plugins search incident`. Read the quality review for findings and the limits of the scan.
With Control running and a saved graph selected, POST candidate graph JSON to `/v1/projects/{project}/graphs/{graph}/change-preview` with its saved `If-Match` ETag. GET `/v1/runs/{run_id}/explanation` for a configured lifecycle's run. See `docs/control-contracts.md` for semantics and client compatibility.

## Checks

- Full suite with disposable local PostgreSQL 17: **2,419 passed**, one existing Starlette/httpx deprecation warning.
- Focused CLI/storage/lifecycle suite: **75 passed**; final CLI test-import adjustment: **10 passed**.
- Updated dependencies: **139 authentication/ingestion tests passed**; strict `runtime-all` dependency audit reports **no known vulnerabilities**.
- Runtime OS layer and Spark/OCI images: linux/amd64 builds and Trivy scans passed with **zero fixable high/critical findings**. User/driver/handler checks passed. Scaffold/release/import tests: **15 passed**; OCI image/publication tests: **10 passed**.
- Ruff lint/format, canonical strict typing (**509 files**), Control contract drift, documentation links, and diff checks passed.
- Compared moved command bodies and identity comparisons structurally against the original code; behavior-bearing expressions match. Protected CI and merge results are available on the task's attached quality-review PR.
- 87 focused comparison, explanation, HTTP, OIDC, and contract tests passed.
- Canonical strict typing passed across 513 files.
- Ruff lint/format, generated Control contract drift, and diff checks passed.
- Protected CI and exact-main verification remain pending for this implementation step. No live cloud workload was run.

## Decisions

- Use the existing lightweight command-dispatch pattern; preserve command options, installer pins, and storage schemas.
- Keep provider-specific SQL and recovery explicit; defer broader extraction until changes demonstrate a shared responsibility.
- Keep static graph comparison separate from infrastructure deployment preview; no new persistence or provider reads.
- Explain existing run evidence deterministically, with no new AI service or fabricated diagnosis.
- Deliver real date-scoped output repair and the guided interface as subsequent steps of the same active objective.

## Remaining

- Large CLI/bootstrap and warehouse/deployment modules remain the next maintenance targets; see the review's specific recommendations.
- Curated connector releases remain separately blocked on the requested exception to the single-writable-repository rule. Prepared patches remain in `/tmp/dander-curated-compat-20260930/` and the preserved connector branch.
- Existing test-client deprecation warning remains; no live cloud qualification was rerun.
- Implement bounded output-partition repair from retained raw data, preserving normal extraction progress and unrelated output rows.
- Integrate connect, configure, preview, run, and outcome in Druff; its repository exception is awaiting the user's answer.
- Publish and consume a verified immutable producer artifact for the paired Druff client; existing published clients use their original bundle.
- Complete protected PR, merge, and exact-main CI for each runtime change.

## Review First

- `docs/quality-review-2026-09-30.md` for the assessment and scope.
- `src/dander/cli/plugins_command.py` and `src/dander/cli/entrypoint.py` for command isolation.
- `src/dander/control/orchestration.py` for the shared run identity rule.
- `src/dander/control/change_preview.py` for graph comparison and write explanations.
- `src/dander/control/run_explanation.py` for evidence-based outcome handling.
- `docs/control-contracts.md` for the API and publication boundaries.
43 changes: 39 additions & 4 deletions docs/control-contracts.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Dander Control contract bundle

Status: the complete hosted-resource bundle is published in Dander `0.9.0rc19` and is the
immutable release artifact Druff may consume
Published client baseline: Dander `0.9.0rc19`. The current source bundle also contains unreleased
additions described below; their presence in this checkout does not publish a new client artifact.

Dander is the authority for data crossing the future Control API boundary. The deterministic
`io.dander.control.contracts/v1` bundle lives in `src/dander/control/contracts/v1` and is included
Expand All @@ -13,12 +13,13 @@ checkout or treat handwritten browser schemas as authoritative.

The bundle contains Draft 2020-12 JSON Schemas and canonical fixtures for graph documents, API
errors, connectors, plugins, operations, deployment previews, graph validation, run requests and
status, bounded logs, mutation results, and capabilities. Each schema has a stable URN `$id` and
status, graph change previews, run explanations, bounded logs, mutation results, and capabilities.
Each schema has a stable URN `$id` and
uses only self-contained `#/$defs` references.

`manifest.json` records every file hash and the bundle SHA-256. The bundle digest excludes the
manifest itself, so the same source models always produce the same identity. The current reviewed
digest is:
digest for the published RC19 baseline is:

```text
695791dfda6058d68453d9e146146d5cdda1439d86c40a7ec249cb4e14a12be3
Expand Down Expand Up @@ -160,6 +161,40 @@ require false all-provider live parity before hosted authentication work began.
These are server-internal storage semantics for DANDER-120. DANDER-121 projects them through the
separately named hosted service while preserving `dander graph serve --file` unchanged.

## Read-only change previews and run explanations

Current source adds two operations for a guided authoring workflow. They use the same Control
authentication, graph limits, and normalized lifecycle as the existing routes:

| Operation | Request | Result |
| --- | --- | --- |
| `graph.change-preview` | `POST /v1/projects/{project}/graphs/{graph}/change-preview`, candidate graph JSON and the saved graph's `If-Match` ETag | Static differences against that exact saved definition; no save, deployment, or warehouse access |
| `run.explain` | `GET /v1/runs/{run_id}/explanation` | Readable outcome and suggested next action from the run's recorded status |

Preview **before saving**: the graph store retains the current definition, not revision history.
The result identifies changed nodes/connections, downstream affected outputs, before/after writer
settings, and all outputs a full run will write. Moving a node on the canvas alone does not mark
outputs as data changes. Removing an output stops future writes but does not delete its existing
table; moving its destination retains the previous table. Unknown row counts and costs stay null.
The preview's two content hashes identify what was compared. Saving and running still use existing
revision checks; editing again requires another preview.

This is a static comparison inside one graph, not a data diff or proof of executability. All
configured outputs execute on a full run; affected outputs are not a selective execution plan.
Existing runtime constraints still apply, including replace-only graph targets. Infrastructure
deployment remains the separate `/deployment-preview` operation and can build/push artifacts.

Run explanations only show counters when the runtime result summary has been collected. An
execution can succeed before those measurements arrive; missing measurements are not zero rows.
Explanations distinguish skipped work, pending cancellation, failed execution, and confirmed
results without guessing an error cause. Suggested actions honor the current run controls; the
explanation does not perform them. Replay is another execution, not rollback or date-range repair.
Neither operation requires provider-specific browser logic or a new service dependency.

Editors can request change previews; viewers can read run explanations when a lifecycle is wired.
Regenerate consumers from the immutable producer artifact after its release. The guided Druff
surface and date-scoped repair are separate follow-on work, not delivered by these two routes.

## Hosted Control service

`dander control serve` is distinct from the existing one-file bridge. It exposes configured
Expand Down
12 changes: 12 additions & 0 deletions docs/decisions.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Engineering Decisions

## 2026-10-01 — Explain pipeline changes and runs through the existing Control boundary

- Compare an unsaved graph with the exact saved revision in a pure, provider-neutral operation.
Keep infrastructure deployment preview separate; no graph history store, warehouse reads, or
new persistence is needed for an author to review a proposed edit.
- Derive readable run explanations from the existing normalized status and collected result
summary. Missing measurements remain unknown; no generated diagnosis or additional AI service
is introduced. Existing start, cancel, and replay semantics remain unchanged.
- Preserve the complete guided-journey objective: the new routes are foundations for Druff,
while date-scoped repair requires its own verified execution behavior. Do not relabel full
replay as date-range repair or advertise an interface before it is implemented.

## 2026-09-30 — Keep command loading and run identity at their owning boundaries

- Plugin commands live in `cli/plugins_command.py`. The console dispatcher selects their group
Expand Down
24 changes: 23 additions & 1 deletion src/dander/control/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
build_typed_operation_catalog,
build_typed_plugin_catalog,
)
from dander.control.change_preview import GraphChangePreviewResponse, compare_graph_changes
from dander.control.graph_store import (
MAX_GRAPH_DOCUMENT_BYTES,
MAX_GRAPH_PAGE_SIZE,
Expand All @@ -36,12 +37,14 @@
LogPageResponse,
MutationResult,
OperationCatalogResponse,
PipelineGraphDocument,
PluginCatalogResponse,
ProjectListResponse,
ProjectSummaryResponse,
RunPageResponse,
RunStatusResponse,
)
from dander.control.run_explanation import RunExplanationResponse, explain_run

if TYPE_CHECKING:
from collections.abc import Callable
Expand Down Expand Up @@ -219,11 +222,14 @@ def capabilities(self) -> CapabilitiesResponse:
"graph.edit",
"graph.delete",
"graph.validate",
"graph.change-preview",
]
if self.preview_port is not None:
operations.append("deployment.preview")
if self.lifecycle_port is not None:
operations.extend(["run.start", "run.read", "run.logs", "run.cancel", "run.replay"])
operations.extend(
["run.start", "run.read", "run.explain", "run.logs", "run.cancel", "run.replay"]
)
return CapabilitiesResponse(
dander_version=__version__,
contract=ContractIdentity(id=BUNDLE_ID, sha256=packaged_bundle_digest()),
Expand Down Expand Up @@ -299,6 +305,18 @@ def preview_graph(
self.require_graph_revision(project, graph, expected_revision)
)

def preview_graph_changes(
self,
project: str,
graph: str,
candidate: PipelineGraphDocument,
*,
expected_revision: str,
) -> GraphChangePreviewResponse:
"""Compare an unsaved candidate with the exact current saved graph."""
baseline = self.require_graph_revision(project, graph, expected_revision)
return compare_graph_changes(baseline.document, candidate)

def start_run(
self,
project: str,
Expand Down Expand Up @@ -342,6 +360,10 @@ def start_run(
def get_run(self, address: RunAddress) -> RunStatusResponse:
return self._require_lifecycle().get(address)

def explain_run(self, address: RunAddress) -> RunExplanationResponse:
"""Explain the same normalized status used by the existing run controls."""
return explain_run(self.get_run(address))

def list_runs(self, *, cursor: str | None, limit: int) -> RunPageResponse:
if not 1 <= limit <= 100:
raise ValueError("The requested run page size is invalid.")
Expand Down
40 changes: 39 additions & 1 deletion src/dander/control/bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
if TYPE_CHECKING:
from pydantic import BaseModel

from dander.control.change_preview import GraphChangePreviewResponse, compare_graph_changes
from dander.control.models import (
ApiErrorEnvelope,
CapabilitiesResponse,
Expand All @@ -28,8 +29,10 @@
ProjectListResponse,
RunPageResponse,
RunRequest,
RunState,
RunStatusResponse,
)
from dander.control.run_explanation import RunExplanationResponse, explain_run
from dander.pipeline.graph import graph_to_payload

BUNDLE_ID: Final = "io.dander.control.contracts/v1"
Expand All @@ -45,6 +48,7 @@
"control-bootstrap": ControlBootstrapDescriptor,
"deployment-preview": DeploymentPreviewResponse,
"graph-create": GraphCreateRequest,
"graph-change-preview": GraphChangePreviewResponse,
"graph-page": GraphPageResponse,
"graph-resource": GraphResourceResponse,
"graph-validation": GraphValidationResponse,
Expand All @@ -55,6 +59,7 @@
"plugin-catalog": PluginCatalogResponse,
"project-list": ProjectListResponse,
"run-page": RunPageResponse,
"run-explanation": RunExplanationResponse,
"run-request": RunRequest,
"run-status": RunStatusResponse,
}
Expand Down Expand Up @@ -402,7 +407,14 @@ def _fixtures() -> dict[str, tuple[str, dict[str, Any]]]:
"minimum_druff_contract": "1.0.0",
"maximum_druff_contract": "1.x",
},
"operations": ["graph.read", "graph.edit", "graph.validate", "run.read"],
"operations": [
"graph.read",
"graph.edit",
"graph.validate",
"graph.change-preview",
"run.read",
"run.explain",
],
"limits": {
"max_graph_bytes": 5 * 1024 * 1024,
"max_page_size": 100,
Expand Down Expand Up @@ -623,6 +635,32 @@ def _fixtures() -> dict[str, tuple[str, dict[str, Any]]]:
"run-request",
{"expected_revision": "opaque-revision", "idempotency_key": "idem-synthetic"},
),
"graph-change-preview": (
"graph-change-preview",
compare_graph_changes(
PipelineGraphDocument(name="new_pipeline"),
PipelineGraphDocument.model_validate(_graph_fixture()),
).model_dump(mode="json"),
),
"run-explanation": (
"run-explanation",
explain_run(
RunStatusResponse(
run_id="run-synthetic",
state=RunState.SUCCEEDED,
stage="complete",
result_schema="io.dander.control.execution-result-summary/v1",
endpoints=1,
extracted=2,
affected=2,
models=1,
assertions=1,
assets=1,
can_replay=True,
logs_available=True,
)
).model_dump(mode="json"),
),
"run-status": (
"run-status",
{
Expand Down
Loading
Loading