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
54 changes: 54 additions & 0 deletions packages/flow/openadapt_flow/bundle_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,60 @@ def _apply_sealed_canonical_omissions(
)


def omit_unset_sealed_defaults(
model: object,
rendered: object,
*,
schema_version: int,
) -> None:
"""Drop listed fields that the sealed file never held, for older contracts.

This reuses the reviewed registry above, more narrowly than the content
digest does. A field is dropped only when all of these hold: its exact
owner and name are on the list for ``schema_version``, the list accepts
``None`` for it, its value is ``None``, and the loaded file did not set it
(it is not in the model's ``model_fields_set``). A field the file wrote,
even as ``null``, stays, and so does any non-``None`` value.
"""

rules = tuple(
rule
for rule in _SEALED_CANONICAL_OMISSIONS_BY_VERSION.get(schema_version, ())
if any(value is None for value in rule.omitted_values)
)
if not rules:
return
_omit_unset_sealed_defaults(model, rendered, rules)


def _omit_unset_sealed_defaults(
model: object,
rendered: object,
rules: tuple[_SealedCanonicalOmission, ...],
) -> None:
if isinstance(model, BaseModel) and isinstance(rendered, dict):
for rule in rules:
if (
type(model) is rule.owner
and rule.field_name in rendered
and rule.field_name not in model.model_fields_set
and getattr(model, rule.field_name) is None
):
rendered.pop(rule.field_name)
for field_name in type(model).model_fields:
if field_name in rendered:
_omit_unset_sealed_defaults(
getattr(model, field_name), rendered[field_name], rules
)
elif isinstance(model, dict) and isinstance(rendered, dict):
for key, value in model.items():
if key in rendered:
_omit_unset_sealed_defaults(value, rendered[key], rules)
elif isinstance(model, (list, tuple)) and isinstance(rendered, list):
for value, rendered_value in zip(model, rendered, strict=True):
_omit_unset_sealed_defaults(value, rendered_value, rules)


def _referenced_asset_paths(workflow: "Workflow") -> set[str]:
"""Every bundle-relative image path the IR references (templates, identifier
crops, postcondition template crops), across the linear steps AND every
Expand Down
2 changes: 2 additions & 0 deletions packages/flow/openadapt_flow/ir.py
Original file line number Diff line number Diff line change
Expand Up @@ -3879,6 +3879,7 @@ def load(
from openadapt_flow.qualification import (
QualificationError,
build_governed_authorization_template,
record_certified_contract_form,
)

try:
Expand All @@ -3893,6 +3894,7 @@ def load(
"governed authorization template does not match the "
"sealed workflow and certification"
)
record_certified_contract_form(wf)

if wf.manifest is None:
wf.manifest = _bv.build_manifest(wf, bundle)
Expand Down
105 changes: 93 additions & 12 deletions packages/flow/openadapt_flow/qualification.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import json
import math
import re
import weakref
from base64 import b64decode, b64encode
from collections import Counter
from datetime import datetime, timezone
Expand Down Expand Up @@ -1195,18 +1196,7 @@ class QualificationError(ValueError):
"""A requested qualification mutation could not be applied."""


def workflow_contract_sha256(workflow: "Workflow") -> str:
"""Digest executable intent and its already-sealed visual assets.

Qualification state and manifest certification metadata are excluded to
avoid self-reference. Existing manifest file hashes bind the plaintext
template assets while keeping their contents inside the declared boundary.
"""

content = workflow.model_dump(
mode="json",
exclude={"manifest", "qualification"},
)
def _workflow_contract_hash(workflow: "Workflow", content: dict[str, Any]) -> str:
payload = {
"workflow": content,
"file_hashes": (
Expand All @@ -1224,6 +1214,97 @@ def workflow_contract_sha256(workflow: "Workflow") -> str:
return hashlib.sha256(encoded).hexdigest()


WorkflowContractForm = Literal["current", "sealed_absent_defaults"]


def _bound_workflow_contracts(workflow: "Workflow") -> set[str]:
"""Workflow-contract digests the persisted qualification is bound to."""

project = workflow.qualification
if project is None:
return set()
bound = {
result.workflow_contract_sha256
for case in project.cases
for result in case.results
}
if project.last_certification is not None:
bound.add(project.last_certification.workflow_contract_sha256)
return bound


def workflow_contract_digest(
workflow: "Workflow",
) -> tuple[str, WorkflowContractForm]:
"""Return the workflow-contract digest and which form produced it.

The current form is computed first and wins whenever the persisted
qualification is bound to it, or to nothing. Only when it is bound to a
different digest is the older form tried: the same content with the
reviewed additive fields the loaded file never held left out
(:func:`bundle_validation.omit_unset_sealed_defaults`). That form is used
only if it reproduces a bound digest exactly; otherwise the current digest
is returned and the caller's comparison fails as before.
"""

from openadapt_flow.bundle_validation import omit_unset_sealed_defaults

content = workflow.model_dump(mode="json", exclude={"manifest", "qualification"})
current = _workflow_contract_hash(workflow, content)
bound = _bound_workflow_contracts(workflow)
if not bound or current in bound:
return current, "current"
omit_unset_sealed_defaults(
workflow, content, schema_version=workflow.schema_version
)
older = _workflow_contract_hash(workflow, content)
if older != current and older in bound:
return older, "sealed_absent_defaults"
return current, "current"


def workflow_contract_sha256(workflow: "Workflow") -> str:
"""Digest executable intent and its already-sealed visual assets.

Qualification state and manifest certification metadata are excluded to
avoid self-reference. Existing manifest file hashes bind the plaintext
template assets while keeping their contents inside the declared boundary.
A certification sealed before a reviewed additive field existed is
reproduced through :func:`workflow_contract_digest`.
"""

return workflow_contract_digest(workflow)[0]


_CERTIFIED_CONTRACT_FORMS: dict[int, tuple["weakref.ref[Any]", str]] = {}


def record_certified_contract_form(workflow: "Workflow") -> str:
"""Record which contract form reproduced ``workflow``'s certification.

Called by ``Workflow.load`` after the governed certification check. Kept
outside the model so it never enters equality or any digest.
"""

form = workflow_contract_digest(workflow)[1]
key = id(workflow)

def _forget(_ref: "weakref.ref[Any]") -> None:
_CERTIFIED_CONTRACT_FORMS.pop(key, None)

_CERTIFIED_CONTRACT_FORMS[key] = (weakref.ref(workflow, _forget), form)
return form


def certified_contract_form(workflow: "Workflow") -> Optional[str]:
"""The form recorded by ``Workflow.load``, or None if none was checked."""

entry = _CERTIFIED_CONTRACT_FORMS.get(id(workflow))
if entry is None or entry[0]() is not workflow:
return None
return entry[1]


def _steps_by_id(workflow: "Workflow") -> dict[str, "Step"]:
from openadapt_flow.traversal import iter_workflow_steps

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"proposed": false,
"step": null,
"reasons": [
"the page that serves the form is not in the capture; start recording before opening it"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"effects_sha256": "50f848801002c21c5e8ee57459c838a42c1db9c8a6d6063bfe095a6d6ca0a76b",
"marker_sha256": "7180c24064c8c02851229f9cd684a3985ddb800eda491b3c20e81d863b740540",
"required_params": [
"DOB",
"city",
"country_code",
"email",
"fname",
"lname",
"phone_home",
"postal_code",
"sex",
"state_label",
"street",
"title"
],
"save_step_id": "step_040",
"schema_version": 1,
"source_recording_sha256": "243fb7853b228a2e0fd93c51c1d43e7b84fff567f1aac917bdcd91a54d044894",
"workflow_name": "openemr-create-synthetic-patient"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
{
"schema": "openadapt.compile-notes/v1",
"note": "Changes the default compiler passes made to demonstrated gestures. Each replaces a gesture that asserted nothing with an equivalent one that carries its own check.",
"changes": [
{
"change": "drop_select_commit_enter",
"step": "step_002",
"evidence": "structural role combobox, then TYPE, then Enter",
"selector": "#form_title"
},
{
"change": "drop_select_commit_enter",
"step": "step_014",
"evidence": "structural role combobox, then TYPE, then Enter",
"selector": "#form_sex"
},
{
"change": "drop_select_commit_enter",
"step": "step_026",
"evidence": "structural role combobox, then TYPE, then Enter",
"selector": "#form_state"
},
{
"change": "drop_select_commit_enter",
"step": "step_038",
"evidence": "structural role combobox, then TYPE, then Enter",
"selector": "#form_country_code"
},
{
"change": "drop_select_all_on_empty_field",
"step": "step_004",
"evidence": "Ctrl+A changed no pixel in the clicked field box",
"region": [
364,
296,
160,
50
]
},
{
"change": "drop_select_all_on_empty_field",
"step": "step_007",
"evidence": "Ctrl+A changed no pixel in the clicked field box",
"region": [
591,
285,
160,
61
]
},
{
"change": "drop_select_all_on_empty_field",
"step": "step_010",
"evidence": "Ctrl+A changed no pixel in the clicked field box",
"region": [
347,
398,
160,
64
]
},
{
"change": "drop_select_all_on_empty_field",
"step": "step_019",
"evidence": "Ctrl+A changed no pixel in the clicked field box",
"region": [
399,
128,
160,
64
]
},
{
"change": "drop_select_all_on_empty_field",
"step": "step_022",
"evidence": "Ctrl+A changed no pixel in the clicked field box",
"region": [
364,
172,
160,
55
]
},
{
"change": "drop_select_all_on_empty_field",
"step": "step_028",
"evidence": "Ctrl+A changed no pixel in the clicked field box",
"region": [
340,
207,
160,
59
]
},
{
"change": "drop_select_all_on_empty_field",
"step": "step_031",
"evidence": "Ctrl+A changed no pixel in the clicked field box",
"region": [
382,
315,
160,
64
]
},
{
"change": "drop_select_all_on_empty_field",
"step": "step_034",
"evidence": "Ctrl+A changed no pixel in the clicked field box",
"region": [
955,
350,
160,
64
]
},
{
"change": "static_control_identifier_crop",
"step": "step_016",
"evidence": "structural role button with no identity evidence",
"region": [
554,
473,
160,
64
]
},
{
"change": "focused_field_region_stable",
"step": "step_027",
"evidence": "structural field role",
"region": [
340,
207,
160,
59
]
},
{
"change": "focused_field_region_stable",
"step": "step_036",
"evidence": "structural field role",
"region": [
937,
210,
160,
56
]
}
]
}
Loading
Loading