onsager-ai/duhem · Archived

verification-authoring

Author Duhem Verification Definitions (criteria + checks YAML) for features under verification. Use when asked to "write a verification", "spec acceptance criteria", "author checks", "verify this feature", "translate criteria to checks", "write criteria", "draft a Verification Definition", or when a Duhem product-surface spec needs a worked example, or when adding a Verification Definition to dogfood a feature on onsager-ai/onsager. Enforces Duhem's Holistic Verification Principle (no mocks of …

First seen Jul 23, 2026

Installation

$ npx skills add onsager-ai/duhem --skill verification-authoring

Summary

  • Author Duhem Verification Definitions (criteria + checks YAML) for features under verification.
  • Use when asked to "write a verification", "spec acceptance criteria", "author checks", "verify this feature", "translate criteria to checks", "write criteria", "draft a Verification Definition", or when a Duhem product-surface spec needs a worked example, or when adding a Verification Definition to dogfood a feature on onsager-ai/onsager.
  • Enforces Duhem's Holistic Verification Principle (no mocks of the web), the criteria-vs-checks separation (criteria stable, checks derivative), and the mechanical-judgment rule (no LLM-in-the-loop verdicts).

Stronger alternatives

This repository is archived — consider an actively maintained alternative.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 1
License LICENSE
Default branch main
Open issues 23
Status Archived

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 23,264 B
  • docs SUMMARY.md 674 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 6 installs

SKILL.md

verification-authoring

Author Verification Definitions for Duhem — the structured YAML artifact containing a feature's acceptance criteria and the checks that mechanically verify them. This is the central authoring discipline that any Duhem-shaped customer (including Duhem itself, when it dogfoods on Onsager) eventually internalizes.

The canonical product spec is in docs/duhem-spec.md, especially:

  • §7 — Core Concepts (verification, criterion, check, assertion,

step, verdict, evidence, artifact, run)

  • §8 — Holistic Verification Principle
  • §10 — Verification Definition (file org, structure, action types,

assertions, runtime expressions, extensibility)

  • §11.2 — Trust Boundary (what AI may and may not do at run time)

This skill is the authoring counterpart to those sections.

When to use this skill

Use when:

  • A Duhem product-surface spec needs a worked Verification

Definition example (the requirement is enforced in issue-spec's Philosophy #4).

  • You're writing a Verification Definition for the Duhem repo

itself (for example, a verification of duhem run's CLI behavior).

  • You're writing a Verification Definition that targets the

Onsager repo as Duhem's first dogfood customer (see also onsager-dogfood for the cross-repo wiring).

  • A user asks to "write criteria" or "translate criteria to

checks" or similar.

Skip when:

  • The change is purely internal scaffolding (build config, repo

hygiene, doc rewording with no schema implication).

  • The user asked for an Onsager spec — that uses Onsager's own

testing patterns; Duhem Verification Definitions for Onsager features live on the Duhem side of the seam (in area:dogfood).

The two-layer structure

Duhem deliberately separates a feature's commitments from the mechanism that verifies them:

Criterion  →  natural language, human-authored, stable across
              implementations.

Checks     →  structured YAML, AI-translatable, frozen after
              human review, one-or-many per criterion.

Criteria are stable; checks are derivative. When the implementation changes, criteria do not. When criteria change, that's a real change to the contract.

When you draft a Verification Definition, write criteria first, then translate each into checks. If you're translating a criterion that already exists (e.g. lifted from a spec issue's ## Test section), don't re-author it — copy verbatim and translate mechanically.

Where the VD lives (Pattern D). A VD that verifies a product (Onsager, Chreode, Crawlab) lives in the product's repo under a co-located .duhem/ suite — Duhem is used as a tool, not a host (docs/duhem-spec.md §10.1 Pattern D; epic #225). Only Duhem's own self-verification VDs live here in verifications/. Chreode is the worked example (onsager-ai/chreode/.duhem/); templates/product-repo/ is the drop-in .duhem/ skeleton (manifest + example leaf + CODEOWNERS) to copy into a product repo. Author the VD there, self-gate it in the product's own CI (Mode A), and let Duhem's drift lane monitor it (Mode B) — see onsager-dogfood.

Authoring loop

0. Scaffold the skeleton: `duhem init --name <slug>` produces a
   runnable Pattern A skeleton (or `--pattern B` for co-located). Its
   target defaults to `./verifications/<slug>/` (Duhem
   self-verification); for a product VD, pass the path explicitly —
   `duhem init --name <slug> .duhem/<slug>` (Pattern D) — or copy
   `templates/product-repo/`. The skeleton is a single passing
   check against https://example.com — your known-good baseline to
   mutate. Spec on issue #48.
1. Lift criteria from the spec / acceptance test / PRD
2. Validate criteria are stable, intent-bearing, scoped to one commitment
3. Translate each criterion to one or more checks (steps + assertions)
4. Review the holistic-environment tax — no mocks of the web
5. Self-validate: every assertion is mechanical, every step is named, every
   ID is referenced
6. Save the file (any name; self-identifies via top-level `verification:`)
7. Update duhem.yml (if Pattern B/C) to register the file

Start every new Verification Definition with duhem init; for a product VD, copy the .duhem/ skeleton in templates/product-repo/ rather than hand-writing a fresh tree from scratch. The skeleton bakes the criteria-vs-checks two-document discipline into the first commit and gives you a passing run to confirm your environment works before you've authored anything.

1. Lift criteria

Find the source of intent for the feature:

  • A spec issue's ## Test items, when the spec is well-formed.
  • A PRD or feature description in plain English.
  • The "what does done mean for this PR" answer the human gives

when you ask.

Each criterion is a single coherent commitment, in 1–3 sentences, that a non-technical stakeholder can read and validate. A feature typically has 2–6 criteria.

A criterion expresses intent, not procedure:

✅ A user can create a workspace from the dashboard. The new
workspace becomes immediately visible in their workspace list,
and the user is navigated to the workspace's home page. No
errors are shown.

❌ When the user clicks the "Create Workspace" button, the
system POSTs to /workspaces with {name, owner_id}, receives
a 200 with the workspace ID, then redirects to
/workspaces/<id>.

The first describes what "done" means. The second describes how — and would have to be rewritten any time the implementation changes. That's a check, not a criterion.

2. Validate criteria

Before translating, sanity-check each criterion:

  • One coherent commitment (not "and also and also …")
  • 1–3 sentences
  • Free of implementation language (no endpoint paths, no

function names, no DB tables)

  • Free of step-by-step procedure
  • A non-technical stakeholder could read it and say yes/no
  • Stable across plausible implementation changes

If a criterion violates any of these, rewrite it before translating.

3. Translate to checks

Each criterion gets one or more checks. A check is a sequence of steps (named actions) followed by assertions (mechanical predicates over named outputs).

A single check should exercise a slice of the holistic web — not a single component. The example in docs/duhem-spec.md §10.3 (AC-1.1) exercises five layers (UI input capture, UI button activation, network observation, API response shape, ID semantics) in one check, and that's the intended shape, not over-reach. Per the Holistic Verification Principle, decomposing a check into per- component sub-checks loses what makes Duhem Duhem.

Retrieval, not recall. Duhem is a v0.x framework no model has a reliable pretraining corpus for — and any corpus would be the wrong version, since the schema breaks by design. So do not guess with: keys or output names from memory. Retrieve the version-exact contract:

  • duhem actions — list the action catalog.
  • duhem describe <uses> — one action's with: fields (+ closed enums),

outputs, and a worked example. (e.g. duhem describe ui/assert-element tells you it produces satisfied.)

  • or read the generated

[docs/action-reference.md](../../../docs/action-reference.md), the same contract for every action in one page.

  • duhem mcp — exposes describe / actions / validate to a coding

agent over MCP, so AI-assisted authoring retrieves the same contract programmatically (#252).

Then let duhem validate close the loop: it field-checks each step's with: keys, outputs: action-fields, and closed-enum values against the same contract and names the valid options on a miss. Author → validate → fix converges to correct without prior Duhem knowledge — retrieval + verification, not pretraining.

Use the existing catalog; if you genuinely need a new action type, that's an area:schema spec — see issue-spec. Do not silently mint new uses: strings.

Terse by default (#267 / #253). Duhem infers the ceremony, so author the minimal form:

  • outputs: is optional. Reference any output an action declares

directly as $steps.<id>.outputs.<name>, including nested paths (…outputs.body.data.id). Add outputs: only to rename a field (outputs: { code: status }) or bind a deep extraction to a short alias (outputs: { projectid: body.data._id }, #273). An identity binding outputs: { foo: foo } is a redundant no-op — validate flags it.

  • assertions: is optional for a judging step. A ui/assert-* or

api/poll step is the judgment (it emits satisfied, implicitly asserted == true), so an all-assert check needs no assertions: block. Bind satisfied and assert it yourself only for manual control. A check with neither assertions: nor a judging step is rejected at validate time.

  • **ui/extract is deliberately non-judging.** It reads an attribute,

DOM property, or text into outputs.value; it emits no satisfied, so it does not satisfy the rule above. A check whose only step is ui/extract needs an explicit assertions: block — extraction observes, assertions: judges, and that separation is the point. Use field: for the everyday cases (checked, value, href, class, aria-, data-); it resolves through a closed documented table, and a name outside that table is a validation error rather than a guess. Reach for attribute: / property: when you need to be exact about which one you mean — they genuinely differ (a ticked checkbox has the checked property true while the attribute may be absent entirely).

  • For a simple state check, prefer ui/assert-element's expect:.

expect: { field: checked, equals: true } judges in one step and keeps satisfied, so you need no step id, no output binding, and no check-level assertion. Reach for ui/extract when the value has to travel — into a later step, or into a check-level assertions: block alongside other evidence.

Common shape (terse):

- id: AC-1.1
  description: <what this check verifies — paraphrase the criterion slice>
  steps:
    - uses: ui/click
      with: {role: "button", name: "Create Workspace"}
    - uses: api/observe
      id: api_call
      with:
        method: POST
        path: /workspaces
        timeout: 3s
      # no outputs: block — status / body resolve directly; add a binding
      # only to alias a deep path, e.g. outputs: { workspace_id: body.id }
  assertions:
    - $steps.api_call.outputs.status == 200
    - type_check:
        value: $steps.api_call.outputs.body.id
        is: uuid

Authoring rules:

  • Every step whose output an assertion reads gets an id:.
  • Reference outputs by their fully-qualified path,

$steps.<id>.outputs.<name>; outputs: blocks are only for a rename or a deep-extraction alias, not for re-declaring native fields.

  • Timeouts (timeout:) are explicit on steps that observe

something asynchronous.

  • Steps default to if: success; use if: always for per-attempt

cleanup and if: failure for failure-only diagnostics. Both still run after a step-level engine error before the original error aborts the run. A retried check runs its cleanup once per attempt, so cleanup steps must be idempotent.

  • Use role-based locators ({role: "button", name: "..."})

rather than CSS or XPath — UI churn invalidates the latter while role-based selectors track the user-visible affordance.

  • A check that has no assertions: is a script, not a check.

Reject it.

  • The capture/ output-name prefix is reserved for runner-emitted

failure evidence (specs #202 / #204): a failing ui check automatically records capture/screenshot + capture/dom + capture/network (a HAR 1.2 log of the page's traffic) blob observations (duhem run --capture controls the policy). An authored output under capture/ is rejected at validate time, and captures are never recorded as $steps.<id>.outputs.* bindings — so nothing can forge a capture and no assertion can bind one. Captures are evidence for humans/agents, never judge input. The network HAR redacts sensitive headers and auth request bodies. Registered secret input values are additionally exact-substring masked from response bodies, DOM snapshots, and other recorded text at the evidence boundary. Screenshots/video remain unmasked pixels, so keep credentials out of rendered UI (or use --capture off).

Share locators instead of copying them

Put locators reused by multiple leaves in an ordinary manifest include:

# pages.yml
pages:
  login:
    username: { role: textbox, name: Username }
    submit: { role: button, name: Sign In }

# leaf step
- uses: ui/type
  with: { locator: $pages.login.username, text: $inputs.user }

The catalog is exactly two levels (page → element), with no locators: container. Root entries win over includes; leaf-local entries win over the composed manifest. A locator may contain $inputs.*, but every leaf receiving it must declare those names. Validate dangling references offline; use duhem resolve --provenance to inspect the effective catalog and each winning source file.

Share step flows instead of copying sequences

Put a repeated action sequence in a flows: catalog on the root manifest, an included fragment, or a leaf. Bind its typed parameters at each call::

flows:
  sign_in:
    params:
      password: { type: string, secret: true }
      user: { type: string }
    steps:
      - uses: ui/type
        with: { locator: $pages.login.username, text: $params.user }
      - uses: ui/click
        with: { locator: $pages.login.submit }
      - id: landed
        uses: ui/assert-element
        with: { locator: $pages.login.welcome, expected: visible }
    outputs:
      signed_in: $steps.landed.outputs.satisfied

- id: login
  call: sign_in
  with: { user: $inputs.user, password: $inputs.password }

Flow step bodies are hygienic: they may reference $params. and $pages. only, never the caller's $inputs. or $steps.. Pass every dependency explicitly. The exception is the flow's outputs: map, where $steps.* selects the inner value exposed to callers as $steps.<call-id>.outputs.<declared-name>. Inner ids are private, and secret: true params are masked like secret inputs. Inspect the flat expanded sequence and flow origins with duhem resolve --provenance.

4. The holistic-environment tax

Per §8, a Duhem check, by default, exercises real behavior end-to-end. No mocking the web. When you author a check:

  • Don't propose an api/mock or db/stub action — they don't

exist by design.

  • Don't write a check that runs against an in-memory test double

of any subsystem the artifact depends on.

  • If a check requires data preconditions (a seeded user, a

pre-existing workspace), use db/seed or event/publish in the verification's setup: block, against the real database / event bus.

  • Use setup: for once-per-verification preconditions; don't

duplicate them inside every check.

If verifying the criterion would require mocking the web, that's either:

  1. A signal the feature's auxiliary assumptions are unstated and

the criterion should be reformulated against the real web, or

  1. A signal that this check is the wrong shape for Duhem and a

different check (or the spec being verified) needs to change.

Don't paper over it with a mock. Stop and escalate.

5. Mechanical judgment

Per §11.2, no LLM is in the verdict loop. Every assertion must be a deterministic predicate evaluable by the judge. Allowed forms (§10.6):

  • Boolean expression: $steps.X.outputs.Y == 200
  • Type check: type_check: {value: ..., is: uuid|string|integer|float|boolean|object|array|null}
  • Pattern match: matches: {value: ..., pattern: ...}
  • Set membership: in: {value: ..., set: [...]}
  • Existence: exists: $steps.X.outputs.Y
  • Cross-step consistency: equal: [$steps.A.outputs.X, $steps.B.outputs.X]

Things that look like assertions but aren't:

  • "The response makes sense" — not mechanical.
  • "An LLM grades the output" — explicitly forbidden by §11.2.
  • "The screenshot looks right" — no L3 visual baseline yet

(docs/duhem-spec.md §14 Phase 3 roadmap).

If a criterion seems to require non-mechanical judgment, that's either a criterion in the wrong shape (split it until each piece is mechanical) or a criterion that genuinely cannot be Duhem-verified today — note that explicitly in the spec, don't paper over it.

6. Self-validate

Before saving, walk through:

  • Each criterion appears verbatim under criteria: with an

id: (AC-1, AC-2, …)

  • Each criterion has at least one check
  • Each check has a non-empty steps: and a verdict — an

assertions: block or a judging step (ui/assert-*, api/poll)

  • Every referenced output is one the action declares (or bound via

outputs: for a rename/extraction)

  • Every assertion is one of the six allowed forms
  • No mock action types
  • No LLM-grading anywhere
  • Action types come from the documented catalog (or are

called out as new in the linked spec)

7. Save and register

The file can be named anything (§10.2 self-identification by top-level verification:/criteria:). Conventional names: <feature>.verification.yml, <feature>.yml, verification.yml.

If the project uses Pattern B/C with a root duhem.yml manifest, add the file to verifications: (§10.4). If it's a standalone file run via duhem run <file>, no manifest update needed.

duhem run discovers the manifest by walking the current directory and its ancestors (capped at the enclosing .git), so cd anywhere-in-the-repo && duhem run finds the repo-root duhem.yml (or .duhem.yml) without a path argument (#69). -f path/to/manifest.yml overrides discovery for an out-of-tree manifest.

Running a leaf directly by path (an explicit positional path or -f/--file) uses this same ancestor walk from the leaf's own directory, not just the no-path case: if it finds a root manifest, it merges that manifest's composed pages:/flows: catalog into the leaf before flow expansion, so $pages. and call: references the leaf doesn't declare locally still resolve. Sibling verifications: entries the manifest lists are never loaded or executed — the run stays scoped to the requested leaf. A leaf with an unresolved $pages./call: name and no discoverable manifest fails naming exactly what's missing and where it would normally be defined (#384/#388).

Worked example template

Use this as the skeleton for any spec that needs a worked example:

verification: <descriptive name>
spec_ref: <link to spec issue or doc>

inputs:
  # named inputs — e.g. test fixture values, defaults
  example_input:
    type: string
    # Defaults are literal; use $runtime.uuid() in a step's with:.
    default: "fixture-workspace"

setup:
  # once-per-verification preconditions — real environment, no mocks
  - uses: db/seed
    with:
      table: users
      rows:
        - {id: $runtime.uuid(), email: "[email protected]"}

criteria:
  - id: AC-1
    description: |
      <verbatim from spec; 1-3 sentences; intent over implementation>
    checks:
      - id: AC-1.1
        description: <what slice of the web this check exercises>
        steps:
          - uses: <action-type>
            id: <step-id>
            with: { ... }
            outputs:
              <name>: <expression>
        assertions:
          - <mechanical predicate over $steps.<id>.outputs.<name>>

Anti-patterns (don't)

  • Criteria with implementation details. "Click the button at

#create-workspace" is a check pretending to be a criterion. Rewrite as "User can create a workspace from the dashboard."

  • A check with no assertions. It's a recording, not a check.

Add the assertion or delete the check.

  • Redundant identity bindings. outputs: { foo: foo } does nothing

now that outputs: is optional (#267) — reference $steps.<id>.outputs.foo directly. Reserve outputs: for a rename ({ code: status }) or a deep extraction ({ id: body.data._id }); validate flags the identity no-op.

  • Mocks anywhere. No api/mock, no db/stub, no

time/freeze short-circuits. The web Duhem verifies is the real one.

  • LLM-grading assertions. Non-mechanical judgment is

explicitly out per §11.2.

  • One mega-criterion. Each criterion should be one coherent

commitment. "User can create, list, edit, archive, and delete workspaces" is five criteria.

  • Re-authoring frozen checks every run. §7.3: checks are

authored once, reviewed, then frozen. Regenerating them from scratch every run reintroduces drift Duhem exists to prevent.

  • Verifications that smuggle in unit tests. A check that

exercises a single function in isolation isn't holistic — it's a unit test in a Verification Definition's clothing. Either expand its scope to a real web slice or remove it.

Relationship to other skills

Related surface Role
issue-spec Specs that introduce product surface link a worked example here. Installed globally from onsager-ai/dev-skills.
[duhem-dev-process](../duhem-dev-process/SKILL.md) Top-level SDD loop — the dogfood discipline that requires worked examples.
[onsager-dogfood](../onsager-dogfood/SKILL.md) Verifications that target onsager-ai/onsager; this skill writes them.
pr-lifecycle (global) Verifies the worked-example check on schema-impacting PRs.

References

  • docs/duhem-spec.md — canonical product spec

- §7 Core Concepts - §8 Holistic Verification Principle - §10 Verification Definition (full schema) - §11.2 Trust Boundary

  • docs/duhem-brand.md — design discipline; rarely directly

relevant to Verification Definitions, but worth reading for the Duhem-Quine grounding (§2 Design rationale)