Skip to content

[FEATURE] Route validated expectations onward into checkpoint orchestration - #12068

Draft
joshua-stauffer wants to merge 28 commits into
developfrom
f/agent-skills/configure-checkpoint-skill
Draft

[FEATURE] Route validated expectations onward into checkpoint orchestration#12068
joshua-stauffer wants to merge 28 commits into
developfrom
f/agent-skills/configure-checkpoint-skill

Conversation

@joshua-stauffer

@joshua-stauffer joshua-stauffer commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

Stacks on #12063. Extends the bundled agent-skill guidance so a session that has already connected a data source and written expectations can go one step further: binding validations into a checkpoint, adding post-run actions, and running it — with the same version-matched, testable-guidance discipline the earlier skills established.

  • Adds a third bundled skill covering checkpoint orchestration: binding data assets and suites into validation definitions, grouping them into a named checkpoint with post-run actions (including the documented plain-string vs. templatable credential-field distinction), and verifying with a single run.
  • Routes the expectations skill's flow onward into this new skill once results are computed, rather than treating a saved suite as a terminal state — the honest "results are not persisted" caveat still comes first, and declining the offer remains a valid end state.
  • Extends the session write-out procedure through validation definitions and checkpoints, rebuilding checkpoints from freshly-persisted objects rather than carrying over stale handles, and reporting a found-not-written outcome so a user is told when an object they'd expect to create already existed.
  • Fixes a write-out step that looked up its target suite by a placeholder literal instead of the session's own suite name — a silent failure that left a project missing its validation definition and checkpoint with no error raised.
  • Extends the installed-artifacts guard's exact expected-skill set to cover the new skill — an exact set rather than a floor, so a wheel carrying an unexpected skill fails it too — and drops the exact skill count from its prose and one function name, so the guard's own wording doesn't go stale at the next skill the way it would have this time.
  • Adds an executable test suite that runs the checkpoint guidance itself (bind, group, add, run) against a real context, asserts a fresh context re-running the persisted checkpoint behaves identically, executes the documented hand-off run snippet as a subprocess, and pins five previously-undocumented failure modes as regression tests (destructive upsert cascades, ordering failures from referencing unpersisted suites, ephemeral-run Data Docs URLs, a silent no-op under a null sites entry, and a mixed temporal-source failure).

Why

Guidance that describes a procedure without anything executing it degrades silently — the moment the underlying API shifts, the guidance is confidently wrong rather than visibly out of date, and nothing catches it. Every new procedural claim added here ships paired with a test that would fail if the claim stopped being true, and the installed-artifacts guard is updated to scale to an arbitrary number of bundled skills instead of re-encoding a specific count that the next skill would immediately go stale.

Note on CI

The installed-artifacts guard step does not execute on this PR: under pull_request_target the workflow definition is read from the base branch, and the step is additionally gated on the PR being non-draft. A skipped or absent run of that step here is not a signal either way — it will run once this PR is out of draft on top of a base branch that already carries the workflow update.

Test plan

  • mypy static type-checking — clean, no issues found
  • Reviewed merge-base diff against the PR's own base branch (not develop) for unrelated changes, secrets, and internal references
  • CI: unit tests, lint, schema checks

The schema generator's expectation list is curated: a generated JSON schema
is only complete if the expectation's class defines the catalog metadata
block (short description, data quality issues, supported data sources).
Five registered expectations define no such block, so emitting schemas for
them would pass off structurally incomplete files as real ones.

Name them explicitly instead, so a completeness check can tell a known,
documented gap apart from an accidental one. Also drop a duplicated entry
from the generator's list; it produced no second file and only invited
confusion about which expectations are covered.
Consumers that want to know what Great Expectations can do currently have
to re-parse every schema file to find out. Emit two generated indexes
alongside the schemas instead, from the same generation step, so they
cannot drift apart.

The datasource index maps each schema file to its exact add_or_update_*
factory method. Most types snake-case cleanly from the class name but six
of twenty-six do not, so the mapping is read from the live type registry
at generation time and frozen into shipped data. Because the method name
is reconstructed from the registered type name, each one is checked
against the real factory surface, so a change to the naming convention
fails here rather than shipping an index pointing at methods that do not
exist.

The expectation index maps each type to its schema file and catalog
metadata, and names the registered expectations that have no curated
metadata block under documented_absent, so a completeness check can tell
a known gap from an accidental one.

Both emitters return their serialized content rather than writing it, so
a drift check can regenerate in memory and compare against the checked-in
file without duplicating the extraction logic it is meant to guard.
A generated file that nothing checks will silently rot. Add assertions
that regenerate each index in memory and compare it byte-for-byte to the
checked-in copy, that every datasource index entry names a factory method
which actually exists, and that every shipped expectation is either
cataloged or explicitly recorded as absent - with the catalog and the
absence list required to be disjoint, so an entry that gains a schema
later cannot linger in both.

The completeness check compares against expectations whose implementation
lives under the great_expectations package rather than the whole registry.
The registry is process-global and test modules register expectations at
import time, so an unfiltered comparison would depend on which other test
modules happened to be collected.

The datasource index guard lives in its own module because the existing
schema test file is skipped on every supported Python, so an assertion
added there could never run.

Hoist the generator's expectation list to a module-level constant so a
drift check can regenerate the index without restating the list, which
would defeat the purpose of the check.
The expectation and datasource schema catalogs describe what this package
can do, and tooling is expected to read them from an installed package
rather than from a source checkout. Until now no JSON shipped at all, so
those catalogs existed only for people working from a clone.

Scope the globs to the two schema trees rather than matching JSON
anywhere under the package, so an unrelated JSON file added elsewhere
later does not silently become part of the distribution.
Resolving the expectation classes by name makes their type opaque to the
checker, so the attribute-defined suppression on the schema call is now
unused and fails the type check under warn-unused-ignores.
Three reference documents an agent consults while operating a session:
finding or announcing the working context, writing an in-memory session
out to a real project, and interpreting slow or failing data operations.

Each procedure was executed against a live context rather than written
from the API surface, which changed several of them:

Neither an unusable GX_HOME nor a discarded cloud configuration produces
any signal at the context entry point - both look identical to having no
project - so the agent checks the environment itself instead of waiting
for an error that never arrives.

Adding a data source under a name that already exists replaces it and
drops every asset on it, so the write-out procedure fetches first and
adds only what is missing, and says plainly what the replacing call would
destroy.

A time budget is a check-in, not a cancellation: the query is already
running on the platform's own compute and keeps costing whether or not
the client waits, so the operation is polled on a worker and the user
decides whether to keep waiting or narrow the data.

Scope reduction leads with a narrower batch definition, because assets
describe what data a project works against and batch definitions are how
it gets sliced. A row limit through a query asset is offered only when no
column supports partitioning, and only as a temporary exploration step.
The entry document carries the guided flow from an empty session to a
batch definition that has been proven to read data. Two behaviors it
encodes are not obvious from the API surface:

Retrieving a batch does not prove a batch definition works. Batch
retrieval is lazy for SQL query assets, so a query over a nonexistent
table returns a batch and only fails when something reads through it.
The flow therefore ends at a head probe, not at retrieval.

Replacing a data source drops every asset on it. Reusing an existing
data source to add one asset would silently destroy the user's other
assets, so the flow fetches first and falls back to the replacing
factory only when nothing is there to reuse. Assets and batch
definitions have no update-in-place factory at all and raise on a
duplicate name, so they reuse or delete-then-recreate.

The catalog reference derives every configurable type, its factory
name, and its asset and batch-definition surface from the shipped
schema index at runtime. The factory naming is irregular in six of
twenty-six cases, and a hand-maintained list would go stale the first
time a type is added.
The entry document turns checks a user describes in words into
expectations drawn from the shipped catalog, runs them, and reports
each result. Four behaviors it encodes are not evident from the API.

A suite must be registered before expectations are added to it.
Expectations added to an unregistered suite validate normally and are
then silently lost, because nothing is wired to a store. The handle
the store returns is the one to build on.

Replacing a suite discards its contents. The upsert factory saves the
incoming suite over the stored one rather than merging, so passing a
fresh empty suite under an existing name zeroes it with no error. The
flow fetches an existing suite and reuses it, and treats replacement
as a separate deliberate act.

Per-expectation results do not come back in the order they were added.
Pairing results to expectations by position appears to work on small
suites and silently mislabels larger ones, so results are paired by
their own configuration.

An empty result means a metric errored rather than data failing, but
only when the expectation also did not succeed: a passing expectation
can carry an empty result too. The two cases are reported differently,
because a broken column is a configuration problem and a failed
expectation is a finding about the data.

Suite names containing dots fan the store out into nested directories,
so they are prohibited.
Agent platforms discover a skill by parsing its frontmatter and
following its relative references, so a skill that drifts out of the
format stops being found rather than failing loudly. These tests hold
the shipped skills to the format: frontmatter parses, the declared
name matches the directory, the description stays within limits, and
every reference resolves to a real file no more than one directory
below the skill root.

The three session references are carried by both skills, because a
skill directory must be self-contained. They are compared byte for
byte, and the failure names the copy direction so drift is a
one-command fix rather than a merge.

Each check ships with a companion test that builds the violation it is
meant to catch and asserts the check reports it. A check that cannot
fail is worse than no check, because it reads as coverage. Discovery
scans for entry documents rather than naming the skills, and asserts
it found some, so an empty scan fails instead of passing quietly.
Guidance that carries code is only as good as the code it carries. The
blocks tagged executable now run in order against a throwaway session
backed by pandas and sqlite, and the run has to reach three end states:
a batch definition that reads data, a validation result carrying an
entry per expectation, and a written-out directory that a fresh
file-backed context loads with its batch definitions and suites intact.
Every other block still has to parse.

The negative paths are pinned too, because each one is a claim the
guidance makes. A query asset over a missing table hands back a batch
and only fails when something reads through it. An expectation whose
metric errored returns an empty payload while one that genuinely failed
returns a populated one -- and since an expectation that passed also
returns an empty payload, both halves of that test are load-bearing.
Empty tables and all-null columns produce results rather than raising,
including the case where a range check passes because nulls count as
missing rather than unexpected. An empty partition window fails at
retrieval, before any probe.

The configure snippet is exercised against a session that already holds
the data source, so it fails if it is ever rewritten to replace rather
than reuse. Replacing a data source drops every asset on it, and a
snippet that teaches the safe form should stop compiling the moment it
stops teaching it.

Result ordering gets the same treatment: results are grouped by column
and expectations whose metric errored are moved to the front, so an
example that pairs results with expectations by position is documenting
a coincidence. The text now describes the mechanism, and the tests hold
it to that.
Agent platforms look for skills in fixed project directories, so the
skills have to be copied out of the package and into the project. The
delicate part is not the copy -- it is that the command has to stay
safe to re-run against a directory that may hold the user's own work.

Each installed skill carries a manifest recording the version that
wrote it and a hash of what was written. That is what makes ownership
decidable. A copy matching its manifest is left untouched, byte for
byte. A copy that still matches its manifest but trails the package is
replaced. A copy that no longer matches has been edited by the user,
so it is refused and left alone until the run explicitly forces it.
A directory with no manifest was never ours and is refused always,
including under force -- ownership is not something a flag can assert.

Ownership is settled before versions are compared, because the reverse
order turns a package having moved on into a licence to overwrite work
the user did by hand.

Writes stage beside the destination and swap in, so an interrupted run
leaves either the previous skill intact or a staging directory the next
run clears -- never a half-written skill. Anything that goes wrong with
one skill is recorded and reported rather than raised, so one
unreadable path costs one destination instead of the whole run.

The package hash covers symlinks by their target rather than by what
they point at, and the walk that feeds it never descends through a
linked directory. Both matter: dereferencing would copy content from
outside the package into the project, and directory recursion through
links varies by Python version, which would make the same tree hash
differently on different interpreters.

Discovery resolves the package through the import system rather than
walking up from a file path, so it works the same for a wheel, an
editable install, and a source checkout. Finding no skills at all is
raised rather than reported: nothing to install is a packaging defect,
and reporting it as a run where nothing went wrong would hide it.
The skills ship inside the package, but agent platforms look for them
in the project. This is the command that bridges the two, invoked as a
module rather than a console script so it needs no entry-point wiring
and cannot collide with anything already on the path.

Install accepts a project root, a target, symlink mode, and a force
flag; list reports what is bundled and what each target currently
holds, naming the version that installed it so a project left behind
by an upgrade is visible before anything is changed. The report is
grouped by outcome and the command succeeds only when nothing failed.

Two details are less obvious than they look.

Failures carry a kind rather than only a message, because the reason a
destination was refused cannot be recovered afterwards by looking at
it. A skill refused for local edits and a skill whose subdirectory
could not be read both still exist and both still hold a valid
manifest. Inferring one from the other means telling a user to hunt
for a stray file when the real problem is a permission bit. Only the
code that made the decision knows, so it now says.

The project root defaults to the working directory lazily rather than
while the arguments are being defined. Reading the working directory
can fail -- it has been deleted or unmounted -- and doing it during
argument definition puts that failure outside the handler that turns
it into a message, so every invocation raised, including the ones
that never needed a directory at all.
The installer decides when to overwrite files in a directory someone
else owns, so these tests check the project on disk rather than the
report the installer returns. A report can say a skill was left
untouched while the bytes underneath it moved; only the filesystem
settles it. Modification times and inodes are compared alongside
contents, because rewriting a file with identical bytes is still a
write.

Every check ships with a companion that breaks the installer in the
one way that check exists to catch, and asserts the check notices. A
test suite that cannot fail is worse than none, because it reads as
coverage.

Building those companions turned up a recurring weakness in the tests
themselves. A helper that sets up more than the behaviour under test
requires quietly removes a dimension from every test that uses it: one
helper always paired an edit with a version change, so nothing could
distinguish a stale skill from an edited one, and deleting that
distinction from the installer left the whole suite green. The same
shape hid three more, including a re-run that asks for the other
install mode and a rebuild that changes content without changing the
version -- the case an editable install lives in.

The helpers now take those dimensions as explicit arguments and
verify them, so a run set up to prove something it does not prove
reports that instead of passing. Each condition the installer tests
before deciding a skill is current or unmodified is now covered by
its own test, one at a time.
The skills are read from the installed package at runtime, so they have to
travel with it rather than exist only in the source tree. Matched by file
rather than by directory, since a package_data pattern only ever selects
files and wheels cannot record an empty directory.
Packaging rules fail silently: a glob that stops matching drops files from
the wheel while every source-tree test stays green. This checks the artifact
users actually install, after installation, so a packaging regression fails
loudly instead of shipping.
Captures why the guidance is version-matched by living inside the
distribution, why the command surface is module invocation rather than a
console script, and why installing copies into a project by default with
an ownership manifest instead of writing over what it did not create.
The rationale is the same; the phrasing no longer dates itself against
whatever the test suite happened to cover at the time it was written.
A missing-driver error names the bare distribution to install, and the
guidance told the agent to relay that message close to verbatim. Following
it resolves a driver version against nothing in particular and leaves the
environment with no record of which backend the dependency was for.

The install step now names the optional dependency group the package
declares for that backend, with the group names read off the installed
distribution rather than recalled — they are version-matched, and several
do not match the backend's common name. Handing the command to the user
rather than running it is unchanged.
Two places where the flow let something happen that nobody chose.

A user who expressed no batching preference fell through to a whole-
collection definition, without ever being told whether the data carried a
column that could slice it. The choice now stays open until the
verification probe runs, whose frame already names every column and type,
and a whole-collection result is reported as something the survey found
rather than something the flow assumed.

The worked examples opened with a bare data-source factory call. That
factory replaces rather than merges, so a copy of the example aimed at a
name that already exists drops every asset attached to it, silently and
with the flow reporting success. The prose caveat above the block does not
survive being copied, so the reuse guard now lives in the blocks
themselves. The asset and batch-definition calls stay bare: those refuse a
duplicate name rather than destroying anything.
Writing an in-memory session out to a project creates a directory on the
user's disk, and `get_context(mode="file", project_root_dir=...)` creates it
silently: no refusal, no prompt, and a return value identical whether it
opened a project or brought one into existence. The guidance already said to
ask first, but said it as prose above a complete, runnable procedure whose
only trace of the requirement was the word "confirmed" inside a placeholder
string. Followed as written, the whole flow — preflight, configure, verify,
write-out — assembles into one program, and a gate that lives between two
snippets in one program is not a gate at all: there is no point in the run
where an answer could arrive. The confirmation is not declined, it never
occurs.

Nothing downstream can catch it. Once the directory exists, discovery finds
it on every later turn and truthfully reports an existing project at that
path, exactly as it would report one the user made themselves. The call site
is the only place this is catchable, so the requirement now lives there:
write-out opens with the two things the user has to have said, states that a
request to "add a data source" is neither of them, and marks the write-out as
its own run beginning from a reply. Preflight's never-scaffold rule no longer
carves out the write-out procedure as an exception — it is the same rule with
its condition met, and being inside that document is not what makes the call
legitimate.

The accompanying test holds the snippets to it: no shipped snippet may call
`get_context(mode="file", ...)` with a directory it chose or with no
directory at all. Both shapes read as ordinary code, and an example path is
the part of a snippet a reader is least likely to revisit, because it already
looks filled in. A placeholder cannot be run as it stands, which is what
keeps the confirmation in front of the call rather than behind it.
…checkpoints

The write-out procedure previously stopped at expectation suites, so a session
that had gone on to build a checkpoint could not be persisted in full.

The two new steps run after suites because a validation definition that
references a suite not yet persisted in the target is rejected. Checkpoints are
rebuilt from the objects just added to the target rather than carried over from
the session: the freshness check resolves handles through the process-global
active context, so any handle belonging to a different context than the active
one is treated as stale. Post-run actions carry over on the rebuilt checkpoint.

The procedure reports three outcomes rather than two. An object already present
under the requested name is left untouched and reported as found, not written,
because the object the user would then run is the one that was already there.
A checkpoint can be written while grouping a validation definition that was
found, so that pairing is disclosed and the name resolved with the user before
the checkpoint is run.
…eckpoint

Continues the guided path: a session that has connected data and written
expectations can now bind those pairs into validation definitions, group them
into a named checkpoint with post-run actions, and verify it with a single run.

Notes on the shape of the guidance:

Checkpoints are added explicitly before the verification run. A run will
persist an unsaved checkpoint whose children are saved, so relying on that
works silently and teaches nothing; the explicit add is stated as a rule.

Reuse is fetch-first throughout. Upserting a checkpoint replaces its
validation-definition membership and its actions wholesale, and passing a
fresh suite object under an existing name replaces that suite's contents.
Validation definitions in the store are not deleted. Whole replacement is the
only sanctioned use of an upsert.

The catalog documents the eight attachable actions. Four accept environment
variable templates in their credential fields; the other four declare those
fields as plain strings, which Great Expectations never substitutes, so a
template is stored and used literally and fails at run time rather than at
setup. Making such a field functional requires a real credential in
checkpoints/<name>.json, which the scaffolded .gitignore does not exclude.
The skill declines to write one there and says so, rather than offering an
opt-in that would put a secret into tracked project config.

Data Docs output is offered in any session type. Editing a project's
configuration to restore a disabled site is gated on the user agreeing first.
Validation results were being presented as a terminal state: the flow ended
with a saved suite and a printed report, leaving no path to a check that
survives the session. The ending now keeps the honest statement that results
are not persisted, then offers the checkpoint skill as the next step.

The offer is a routing statement, not an obligation -- declining is an
explicit end state, and the write-out offer for in-memory sessions still
comes first. The closing scope list drops orchestration, which is no longer
out-of-family; only rendering results elsewhere remains outside the flow.
Three contracts rested on markdown that nothing checked, each failing
silently. A skill shipping without one of the shared session references
would go unnoticed: the byte-equality check compares copies that exist, so
a missing one has nothing to compare against. A post-run action added to
the package would leave the documented action catalog quietly incomplete.
And a skill named in prose as a hand-off target could be misspelled or go
stale, routing a user nowhere.

Adds a carriage check, an action-catalog drift check pinned to the shipped
registry and scoped to this package's own registrations, and a check that
skill names written in prose resolve to real bundled directories. Reference
extraction now covers fenced content, where a wrong path previously passed
unseen. The bundled-skill floor rises to three.

Each check is paired with a test that introduces the corresponding
violation into a throwaway copy and asserts the check reports it.
The write-out procedure looked its target suite up as
suites.get("my_suite"), but nothing ever persists a suite under that
name. Unlike the data source, asset and batch definition -- which the
procedure creates under the very names it later fetches -- the suite
arrives from the session already named, so the literal only matched by
coincidence.

The failure was silent rather than loud: the surrounding loop records a
failed step and continues, so a user following the procedure ended up
with a project missing both its validation definition and its checkpoint,
with no error raised. Fetching by the session suite's own name is the
only lookup that holds regardless of what the suite is called.
The checkpoint guidance was prose that nothing executed. These tests run
it: bind, group, add explicitly, run, and assert per-validation-definition
and per-expectation entries; a fresh context reruns the persisted,
written-out checkpoint unmodified; and the handoff run snippet executes as
a subprocess with its exit code asserted for both a passing and a failing
checkpoint.

Five traps are pinned exactly as observed, so guidance that drifts from
behavior turns a test red rather than misleading a reader: the destructive
add_or_update cascade in all three of its directions, including the
fetch-first path that makes the documented rule safe; the ordering failure
behind referencing an unpersisted suite; an ephemeral run producing local
Data Docs URLs; the silent no-op under a null sites entry and its
consent-gated recovery; and the mixed temporal-source failure mode.

The round trip executes the shipped write-out text itself rather than a
copy of it, so the procedure cannot regress while the test keeps passing.
The installed-artifacts guard asserted an exact set of two skills, so the
checkpoint skill had to join it: that exact-set assertion is what catches a
packaging glob which fails to pick up a new skill directory, and a wheel
that ships a skill without its entry document installs nothing at all while
exiting zero.

The guard's own prose and one function name encoded the count as well, which
would have gone stale again at the next skill. Both now describe the bundled
skills without numbering them. The nearby references to the two catalog
indexes are a different count and stay as they are.

The skills command help now mentions checkpoint orchestration alongside data
sources and expectations.
Two references named step 5 as where the explicit checkpoint add happens.
Step 5 explains why step 4 adds explicitly; the add itself is in step 4. An
agent following the numbering to find the call would land on prose instead
of code.
Base automatically changed from f/agent-skills/gate-write-out-scaffolding to develop August 17, 2026 10:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant