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
12 changes: 12 additions & 0 deletions .github/workflows/release-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,20 @@ jobs:
run: |
set -euo pipefail
python -m pip install 'build==1.6.1' 'twine==7.0.0'
if [ "$PACKAGE" = flow ]; then
# Flow commits a reviewed list of artifact paths, not their
# hashes, so parallel pull requests don't collide on hash rows.
# The hashes are pinned here, on the exact commit being built,
# and the archives must match them below.
(cd packages/flow && python scripts/check_release_consistency.py --stamp-release-inventory)
fi
python -m build "packages/$PACKAGE" --outdir dist
python -m twine check --strict dist/*
if [ "$PACKAGE" = flow ]; then
# Open-core, license and artifact-provenance boundary of the two
# files the publish job uploads.
(cd packages/flow && python scripts/check_release_consistency.py --validate-dist-dir ../../dist)
fi
if [ "$PACKAGE" = capture ]; then
# Capture's release contract: notes for this version, and archives
# that carry only the public source boundary.
Expand Down
19 changes: 17 additions & 2 deletions packages/flow/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ pytest -q # tests
hooks in `tests/conftest.py` snapshot the tracked-file status before the
first test and after the last one, and fail the run when a new entry
appears, because a regenerated golden can be committed by accident and
`scripts/check_release_consistency.py` pins a reviewed SHA-256 inventory of
the public files. Write to `tmp_path` (copy a fixture bundle there first)
the release workflow pins the SHA-256 of every public artifact file. Write to
`tmp_path` (copy a fixture bundle there first)
instead. The check reports any tracked file that changed during the run, so
editing files yourself while a long suite runs also trips it — set
`OPENADAPT_FLOW_ALLOW_DIRTY_TREE=1` for that case.
Expand All @@ -60,10 +60,25 @@ claims checks:
```bash
python scripts/check_release_consistency.py
python scripts/validate_claims.py --check --structure-only
python scripts/check_release_consistency.py --stamp-release-inventory
uv build --wheel --sdist
python scripts/check_release_consistency.py --require-dist
git restore public-artifacts.json
```

`public-artifacts.json` lists, by path, every file that can carry data,
evidence, static assets, models or configuration (`.json`, `.yaml`, `.png` and
the other suffixes it names). A pull request that adds, removes or renames such
a file runs `python scripts/check_release_consistency.py
--write-public-artifact-inventory` and commits the changed list; editing an
already-listed file needs no inventory change. The list deliberately carries no
hashes: hash rows changed with every edit to `claims.yaml` or a fixture, so
parallel pull requests conflicted on them. The bytes are pinned at release
instead. The release workflow runs `--stamp-release-inventory` on the exact
commit it builds, which rewrites the list with each file's SHA-256, and then
validates the wheel and sdist against it. The open-core path, signature and
license checks run unchanged on every pull request.

After that pull request merges, start the release from protected `main`:

```bash
Expand Down
1,206 changes: 0 additions & 1,206 deletions packages/flow/claims.yaml

This file was deleted.

59 changes: 59 additions & 0 deletions packages/flow/claims/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Capability claims

One file per claim: `claims/<id>.yaml`, where `<id>` is the claim's `id`.
Adding a claim adds a file, so two pull requests that each add a claim don't
edit the same lines. `scripts/validate_claims.py` loads every `*.yaml` here,
enforces the contract below, and regenerates `docs/VERIFICATION.md` and
`docs/verification.json`:

```bash
python scripts/validate_claims.py --check --structure-only
python scripts/validate_claims.py --report
```

The report is a deterministic function of these files and the evidence they
cite. It has no timestamp, claims are grouped by tier and sorted by id, and it
has no counts, so two pull requests that add different claims usually merge
cleanly. `tests/test_validate_claims.py` fails when the committed report is
stale.

## Contract

```text
Every public capability claim about openadapt-flow ("web is supported today",
"Windows has scoped acceptance") must be a FUNCTION of automated
evidence, not a hand-written adjective that quietly rots. This registry maps
each claim -> an evidence `tier` -> the specific backing test(s)/benchmark(s)
that prove it. `scripts/validate_claims.py` enforces the tier<->evidence
contract in CI and regenerates `docs/VERIFICATION.md` from it.

Evidence strength is derived from the repo, not asserted here:
* a test file with NO module-level env skipif, that exists -> "supported"
candidate evidence; its required `test` or `e2e-browser` job must bind the
file to at least one passing case in that job's real JUnit result
* a test file gated by a module-level `pytestmark` env skipif -> "validating"
(opt-in / infra-gated: grounded, but never runs on default CI)
* a doc / benchmark artifact (.md or a benchmark dir) -> "roadmap"
(design/field evidence; cannot by itself prove a running capability)
A claim FAILS the gate when its `tier` OUTRANKS its strongest evidence
(rank order research < roadmap < validating < supported). Additionally a
claim carrying `reproducibility: field` may NOT be tier `supported` (a
result that is not CI-reproducible is never presented as "supported").

TIERS
supported — bound to a real passing result in required default CI.
validating — grounded in a REAL opt-in/infra-gated proof or a field test;
being validated (incl. with design partners). NOT "supported".
roadmap — designed/specified, not yet proven by a running test.
research — open question / study.

EVIDENCE ENTRY FIELDS
path — repo-relative path to a test file, benchmark dir, or doc. MUST exist.
proves — what THIS artifact actually demonstrates (honest scope).
kind — optional: test | benchmark | doc. Inferred when omitted
(tests/**/*.py -> test, *.md -> doc, else -> benchmark).
node — optional: a `def <name>` that must exist in `path` (rot guard).

The gate detects opt-in gating from each test's source (a module-level
`pytestmark` skipif on an env var) — the registry cannot lie about it.
```
81 changes: 81 additions & 0 deletions packages/flow/claims/citrix-pixel-validating.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
id: citrix-pixel-validating
claim: >-
Citrix uses a dedicated exact-Workspace-window backend beneath the governed
pixel-only remote-display contract. Required CI covers its readiness gate,
governed execution, durable resume, report binding, and refusal contracts;
the counted no-DOM qualification covers the driver, visual ladder,
independent effects, and drift halts. Live ICA/HDX acceptance remains
separately bound to the exact deployment.
surfaces: [README.md, docs, website]
tier: validating
evidence:
- path: tests/e2e/test_citrix_pixel_e2e.py
proves: >-
OPT-IN pixel-only proof: structural_armed_coverage == 0, replay
resolves on template/ocr/geometry only, on-screen OCR read-back
verifies the write, the identity gate HALTs on a look-alike patient,
and render drift triggers halt-on-ambiguity.
- path: tests/test_pixel_identity_probe.py
proves: >-
The pixel-substrate identity probe behind the on-pixels identity gate.
- path: tests/test_citrix_workspace_backend.py
proves: >-
Required CI covers the dedicated backend preset, exact owner/title
overrides, readiness probe construction, pixel-only capability
boundary, backend factory, and product CLI selectors.
- path: tests/test_run_gate.py
proves: >-
Required CI refuses governed Citrix execution before action when its
readiness binding is absent or blank, admits a sealed bundle carrying
a recorded readiness binding, and reports only the resolved backend
token rather than sensitive target strings.
- path: tests/test_cli_new_commands.py
proves: >-
Required CI restores the recorded Citrix owner, exact title, and
readiness binding through durable approve/resume, and refuses before
backend construction when resumed configuration is incomplete.
- path: tests/test_hosted.py
proves: >-
Required CI binds a completed local Citrix report to the closed
`citrix` execution token without copying target owner, title, or
readiness values into the hosted summary.
- path: tests/e2e/test_citrix_workspace_standin_e2e.py
proves: >-
The dedicated Citrix backend passes three healthy effect-confirmed
record->compile->replay trials and three severe-drift safe-halts over
a no-DOM surface, with zero model calls, false completion, or silent
incorrect success.
- path: tests/test_citrix_real_acceptance.py
proves: >-
Required CI validates the public real-ICA campaign preflight,
distinct authority keys, executable and oracle attestations, one-use
nonce journal, crash recovery, signed display and monitor-topology
observation, explicit reliability metrics, uncertain dispatch, and
fail-closed report contract without provisioning infrastructure.
- path: benchmark/citrix_workspace/results.json
kind: artifact
proves: >-
The retained six-trial record reports code_readiness_accepted=true
and ica_hdx_accepted=false, keeping driver readiness distinct from a
counted live ICA/HDX qualification.
- path: docs/desktop/CITRIX_PIXEL.md
kind: doc
proves: >-
The Citrix driver model, reusable evidence, exact-deployment
acceptance contract, independent-effect boundary, and
customer-controlled posture.
caveats:
- >-
PIXEL-ONLY: no UIA/DOM crosses to the driver. On-screen OCR read-back is
SAME-SURFACE, not an independent system-of-record check.
- >-
A Parallels VM window is a Citrix ANALOG; real HDX/latency/DPI/lock-
screen drift is not simulated. OPT-IN + infra-gated
(`OAFLOW_CITRIX_PIXEL_E2E=1` or
`OAFLOW_CITRIX_STANDIN_E2E=1`); neither live-window proof runs on
default CI, and neither replaces a counted live ICA/HDX acceptance
record.
- >-
The public real-acceptance harness contains no customer recipe,
credential, identifier, screenshot, or deployment-specific threshold.
A private qualified configuration supplies those exact bindings.
36 changes: 36 additions & 0 deletions packages/flow/claims/compose-recorded-bundles.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
id: compose-recorded-bundles
claim: >-
openadapt-flow compose sequences named, already-compiled child bundles
under a handoff contract. Each child stays bound to its recorded
surface. The parent starts a child only after a predecessor ends
VERIFIED (or an explicit allowed halt class). Handoffs copy
effect-bound parameter values from confirmed effect receipts; missing
evidence HALTs. certify and run execute the parent artifact.
surfaces: [README.md, docs]
tier: supported
evidence:
- path: tests/test_compose_authoring.py
proves: >-
Authoring copies two child bundles, records the handoff contract,
and refuses a source that is not effect-bound, an unknown target
parameter, a single child, a cyclic after-graph, and a backwards
handoff.
- path: tests/test_compose_runtime.py
node: test_two_child_fixture_second_is_local_backend
proves: >-
A two-child fixture runs intake through MockMed with independent
effect verification, then a local FakeBackend child that receives
the verified patient_id. Missing handoff evidence and an
unverified predecessor HALT before the next child starts.
- path: tests/test_cli_compose.py
proves: >-
The compose parser dispatches, the CLI writes a composition
artifact, and replay refuses to run one.
caveats:
- >-
The parent sequences admitted child runs. It does not enlarge the
workflow-program graph and does not introduce a process contract.
It does not switch backends inside one recording.
- >-
Evidence is unit tests plus a local mock second child. This claim
does not cover Production admission or a live Citrix session.
28 changes: 28 additions & 0 deletions packages/flow/claims/deferred-report-check.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
id: deferred-report-check
claim: >-
A save that only a later report can confirm (an X12 999 or 277CA
acknowledgment, a CSV export, a saved status page) ends
SAVED_AWAITING_CHECK: never VERIFIED or billable until a matching
accepting report row settles it, and RECONCILIATION_REQUIRED when the
report rejects it or no row arrives by the deadline.
surfaces: [docs]
tier: supported
evidence:
- path: tests/test_deferred_report.py
proves: >-
Synthetic 999, 277CA, CSV and text reports parse into accepted,
rejected, pending and conflicting rows (a 999 can only reject; a
provider-level 277CA trace is ignored); the ledger keeps keys only as
private digests; a real replayer run ends SAVED_AWAITING_CHECK and
not billable; an unchecked second effect, a failed run or anything
else uncertain stays RECONCILIATION_REQUIRED; an accepting row
settles VERIFIED, while a rejecting row, a duplicate key, the
deadline (checked before any report), a report older than the save,
or a report read another way does not; the CLI settles it and never
rewrites report.json; verify-run refuses the kind.
caveats:
- >-
The report confirms acceptance of the item with that key, not its
field values. The hosted runner and the Execute v1 and
production-receipt wire formats carry the outcome as
RECONCILIATION_REQUIRED.
37 changes: 37 additions & 0 deletions packages/flow/claims/desktop-readback-separate-session.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
id: desktop-readback-separate-session
claim: >-
A save in a desktop, RDP or Citrix application can be checked the way a
person checks it: a separate session that is never the one that typed
searches for the record, opens the one result that shows its
identifier, confirms the record view shows that identifier, and reads
the saved fields through UI Automation or OCR, judged by the shared
effect judge. The look-up types only into the search box and never
clicks a control with a save-like label. With its own sign-in it
declares tier 2 (a separate session); without one, tier 3. It is never
labeled a system-of-record read.
surfaces: [docs]
tier: supported
evidence:
- path: tests/test_desktop_readback.py
proves: >-
On the WinForms patient-notes fixture through the real in-guest
agent and Windows client (UI Automation): confirms a saved note,
refutes a wrong one, opens only the row that shows the full
identity past a same-first-name decoy, halts on two matching rows,
and refuses a search button with a save-like label. On the RDP
kiosk's look-up panel through the RDP backend and real OCR:
confirms a booking from its own RDP logon (tier 2), never turns an
OCR mismatch into REFUTED, and never takes "not found" as proof
that nothing was saved. The same look-up confirms through a Citrix
Workspace window and a native macOS window (tier 3, a window as the
same user). Neither fixture's save control or form is touched. The
deployment refuses a read-back session that is the session that
types.
caveats:
- >-
The WinForms fixture runs as a UI Automation stand-in that mirrors
patient_notes.ps1 and its pn_db.py persistence, and the kiosk as
pixels drawn at the Tk layout; neither is the live Windows VM or the
Docker RDP fixture in this evidence. On the pixel path OpenAdapt cannot
prove where keyboard focus is; it checks the search box shows the
query before it submits.
41 changes: 41 additions & 0 deletions packages/flow/claims/desktop-recording-validating.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
id: desktop-recording-validating
claim: >-
The product CLI records desktop workflows through openadapt-capture and
`record --backend windows|macos|linux|rdp|citrix` captures the operator's
real demonstration and emits the compile-ready recording format, so
record -> compile -> replay closes on the desktop substrate, not just the
browser.
surfaces: [README.md, docs, website]
tier: validating
evidence:
- path: tests/test_desktop_record.py
proves: >-
The live-capture orchestration + CLI wiring (record --backend
windows|macos|linux|rdp|citrix -> openadapt-capture ->
convert_capture), and that a desktop-shaped recording COMPILES into a
bundle and REPLAYS to completion through the desktop backend path,
resolving each click to its recorded target (runs on default CI).
- path: tests/test_capture_adapter.py
proves: >-
The capture->recording bridge over a REAL openadapt-capture session
(its own event-processing pipeline + frame extraction) feeding the
UNMODIFIED compiler (runs on default CI: the `test` job installs the
`capture` extra; openadapt-capture >=0.5.4 imports clean headless).
- path: docs/desktop/RECORDING.md
kind: doc
proves: >-
The capture-assisted and live-observer paths, fail-closed secret
handling, RDP coordinate binding, and reuse of
openadapt-capture + the capture adapter.
caveats:
- >-
Offline capture carries NO structural (UIA AutomationId) locator, so
replay uses the visual ladder (template/ocr/geometry). The live capture
needs a display (openadapt-capture is the optional `capture` extra); the
CI proof covers a real on-disk capture session -> convert -> compile ->
replay, not a live recorder run (the live recorder has its own
integration suite in the openadapt-capture repo).
- >-
`--secret` is refused on an unqualified pixel/desktop capture with no
field geometry, and RDP recording uses the replay pixel space or a
deployment-calibrated client-window mapping.
25 changes: 25 additions & 0 deletions packages/flow/claims/deterministic-zero-model-replay.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
id: deterministic-zero-model-replay
claim: >-
Replay is deterministic, local, and $0 — zero model calls on either the
record or the replay side.
surfaces: [README.md, website, docs]
tier: supported
evidence:
- path: tests/e2e/test_record_compile_replay.py
proves: >-
Baseline replay x3 resolves every step on the `template` rung with 0
heals and 0 model calls; healed bundles replay clean afterward.
- path: tests/test_replayer.py
proves: >-
The replayer is a deterministic resolution ladder; the VLM rung is a
fallback, not on the default path — the core runtime is model-free.
- path: benchmark/BENCHMARK.md
kind: benchmark
proves: >-
CI-reproducible MockMed run artifact: 100/100 compiled replays at
4.9s p50 with zero model calls, measured 2026-07-08 on Flow 0.1.0
(pre-v0.2.0 source build); field/artifact reference.
caveats:
- >-
A VLM rung exists as the lowest fallback of the ladder; it is not
invoked on a clean deterministic replay, and never on the record side.
Loading
Loading