Skip to content

feat(compiler): author data-driven LOOP from a single demonstration - #188

Merged
abrichr merged 1 commit into
mainfrom
feat/data-driven-loop-authoring-20260720
Jul 20, 2026
Merged

feat(compiler): author data-driven LOOP from a single demonstration#188
abrichr merged 1 commit into
mainfrom
feat/data-driven-loop-authoring-20260720

Conversation

@abrichr

@abrichr abrichr commented Jul 20, 2026

Copy link
Copy Markdown
Member

What

Closes the missing authoring wire from a single-demonstration linear bundle to a governed, data-driven "run this workflow for each record in a worklist" LOOP.

Per the core capability review (.private/core_capability_review_2026_07_20.md): the runtime already executes a LOOP over a worklist safely — bounded, $0, zero-model, identity-gated and effect-verified per iteration, halt-on-ambiguity (replayer._exec_loop_state / _interpret_program). The only missing piece was the compile-time step that produces a ProgramGraph with a LOOP from a demonstrated linear body. This PR adds exactly that — and nothing more. It reuses lift_to_program, LoopSpec/Relation, and every existing safety gate verbatim: no new runtime, no new IR, no new safety surface.

New vs reused

  • New (~250 LOC): openadapt_flow/compiler/loop_authoring.py (author_data_driven_loop + explicit/validated column→param mapping), a for-each CLI subcommand, tests, and a generator script.
  • Reused verbatim: the LOOP interpreter, per-iteration identity re-resolution, per-iteration effect verification, max_iterations bounding, fail-safe HALT, and lift_to_program (body scaffold).

Authoring UX

openadapt-flow for-each <single-demo-bundle> \
    --records worklist.csv \        # or .json
    --out my-loop-bundle \
    [--map <column>=<param> ...] \  # explicit mapping; identity by default
    [--max-iterations N]

Every worklist column must map to a known non-secret parameter, and every parameter the body binds must be supplied by the worklist or carry a demo default — otherwise authoring fails loudly (LoopAuthoringError), never emitting a silently under-bound bundle.

Real multi-record evidence (through the actual Replayer, zero model calls)

  • Healthy 3-record run: body runs once per record binding each row; each record's consequential write is independently effect-verified against the in-process MockMed system of record; report.model_calls == 0; bounded.
  • Poisoned record → safe HALT: (a) a GUI drift that writes the wrong patient is caught by the param-bound effect verifier → HALT (the following record never runs — no silent skip); (b) a live identity band naming a different entity trips the identity gate → HALT with no wrong click.

program:true shipped bundle

docs/showcase-loop/bundle is the first shipped program:true bundle (both prior showcases were program:false), generated by scripts/build_showcase_loop_bundle.py from the OpenEMR showcase — proof the wire is reachable end-to-end on a real bundle. PHI-free, guarded in CI.

Honest remaining gap

The worklist here is a provided dataset. When records must instead be read off the screen mid-run (e.g. "process every row in this on-screen queue"), that needs a screen→value extraction primitive that does not exist yet (ActionKind has no read/extract; entity_ref run-time re-resolution is a slot). This PR covers the provided-worklist case; the on-screen-worklist case is future work.

Checks

  • ruff (lint + format), mypy (plain + strict-safety — loop_authoring.py added to the strict scope) green locally.
  • tests/test_loop_authoring.py (13 tests) + related suites (test_program_ir_phase2, test_induction, test_phi_governance, test_consistency, test_ci_workflow_contract, test_cli_*, test_replayer) green.
  • scripts/check_bundle_phi.py passes over all git-tracked bundles including the new one.

Coordination: depends only on the IR; does not touch the parallel program-visualizer work.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM

Close the missing authoring wire from a single-demonstration linear
bundle to a governed, data-driven "run this for each record" LOOP.

The Phase-2 interpreter already executes a LOOP over a worklist safely
(bounded, $0, zero-model, identity-gated and effect-verified per
iteration, halt-on-ambiguity). The only missing piece was the
compile-time step that emits a ProgramGraph whose single LOOP iterates a
demonstrated linear body once per record. This adds exactly that and
nothing more -- it reuses lift_to_program, LoopSpec/Relation, and every
existing safety gate verbatim; no new runtime, no new IR, no new safety
surface.

- openadapt_flow/compiler/loop_authoring.py: author_data_driven_loop()
  wraps a linear body in one LOOP over a Relation built from a worklist,
  binding each record's columns to the body's ParamSpec slots. The
  column->param mapping is explicit and validated -- an unmapped column,
  a mapping onto an unknown or secret param, a bound param with no column
  and no demo default, a ragged worklist, or a worklist longer than the
  loop bound all FAIL LOUDLY (LoopAuthoringError), never emit a bundle.
- CLI: `openadapt-flow for-each <bundle> --records <csv|json> --out ...`
  with repeatable `--map COLUMN=PARAM`, `--relation`, `--max-iterations`.
- tests/test_loop_authoring.py: authoring-shape + fail-loud unit tests,
  plus real-Replayer evidence -- a healthy 3-record run confirms each
  record's write INDEPENDENTLY against the in-process MockMed system of
  record with ZERO model calls and stays bounded; a poisoned record
  (effect the SoR refutes, and identity mismatch) triggers a safe HALT
  with no wrong write and no silent skip; and the shipped bundle is a
  valid program:true loop.
- docs/showcase-loop: the first shipped program:true bundle, generated by
  scripts/build_showcase_loop_bundle.py from the OpenEMR showcase (proof
  the wire is reachable end to end on a real bundle). PHI-free .gitignore
  exception, mirroring showcase-openemr.
- ci: add loop_authoring.py to the mypy-strict-safety scope.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM
@abrichr
abrichr merged commit f9f8bac into main Jul 20, 2026
12 of 14 checks passed
@abrichr

abrichr commented Jul 20, 2026

Copy link
Copy Markdown
Member Author

CI note — the two red jobs are pre-existing and unrelated to this change

All gates this PR affects are green: lint, mypy-strict-safety (this PR adds loop_authoring.py to the strict scope), phi-guard, docs-consistency, interop-types, python-compatibility, windows-mock, linux-atspi-x11, e2e-browser, gate. My new suite tests/test_loop_authoring.py (13 tests) is among the 2586 passing in the test job.

Two jobs are red for reasons this branch does not touch (this commit changes only compiler/loop_authoring.py, compiler/__init__.py, __main__.py, tests, docs, ci.yml, .gitignoreno identity/vision/resolver/packaging source):

  1. wheelcheck_release_consistency.py: error: source distribution contains a link/device/special member: 'paper/workshop/references.bib'. That path is a symlink already on origin/main (mode 120000), added by the paper work; the sdist checker rejects any symlink. Deterministic (fails in 13–21s), independent of this PR.
  2. test — a single failure, tests/test_identity_ladder.py::test_blocker2_mismatch_is_scale_invariant, which renders AC50061 vs AC5OO61 (O/0 homonym) via PIL's fallback font and asserts pixel-identity mismatch at cell width 240. It abstains under CI's font stack (a font-rendering/threshold sensitivity in the pixel-identity verifier). It passes locally on macOS and reproduces deterministically in CI on rerun. It lives entirely in identity-ladder code this PR does not modify (owned by the pixel-identity workstream).

Neither failure is in scope for this PR; both predate it. Happy to rebase once main's wheel-symlink and the identity-ladder font sensitivity are addressed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant