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
30 changes: 15 additions & 15 deletions clara_workflow/agent_instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ Two invocation modes are supported.
```

Treat the argument after `verify` as the requested term id. Accept either
`CL_4033094` or `CL:4033094`.
`CL_4033094` or `CL:4033094`, but normalise it to the CURIE form (`term_id`,
e.g. `CL:4033094`) for everything that follows, including output paths.

### CI / batch

Expand All @@ -33,10 +34,10 @@ JSON file from the repository root.
Treat this routed payload as the stable consumer contract for PR-triggered
CLARA review.

Normalise ids as follows:

- underscore form (`CL_4033094`) is the runtime `cell_id`
- CURIE form (`CL:4033094`) is the routing payload `term_id`
Every id downstream of the routing payload is the CURIE form (`term_id`, e.g.
`CL:4033094`), including output paths (`runs/{term_id}/...`). A colon is a
valid POSIX filename character, so this needs no further encoding on the
runner.

The routing payload contains `targets`. Each target has one of these routes:

Expand Down Expand Up @@ -93,7 +94,7 @@ Relevant target fields:
- Group all targets by `term_id`.
- Process each term group independently.

For every processed term, write one output bundle under `runs/{cell_id}/`.
For every processed term, write one output bundle under `runs/{term_id}/`.

## Assertion preparation

Expand Down Expand Up @@ -257,7 +258,7 @@ For each assertion that has searchable refs in scope:
Asta ids (`PMID:12345`, `DOI:10.xxxx/yyy` — uppercase the prefix, strip
any leading slash on `doi:/...`)
- `limit`: 10 is usually enough
3. Log the call by appending one JSON line to `runs/{cell_id}/tool_calls.jsonl`.
3. Log the call by appending one JSON line to `runs/{term_id}/tool_calls.jsonl`.
Include at least:

```json
Expand All @@ -279,7 +280,7 @@ Trigger only for `core` assertions still `fail` or `uncertain` after Stage B.
Group unresolved `core` assertions by reference paper. For each paper:

1. Call `mcp__artl-mcp__get_europepmc_full_text`.
2. Log the call to `runs/{cell_id}/tool_calls.jsonl`:
2. Log the call to `runs/{term_id}/tool_calls.jsonl`:

```json
{"tool":"get_full_text","target_id":"...","identifier":"PMID:...","available":true}
Expand All @@ -293,17 +294,16 @@ Group unresolved `core` assertions by reference paper. For each paper:

For each processed term, write:

- `runs/{cell_id}/verdicts.json`
- `runs/{cell_id}/tool_calls.jsonl`
- `runs/{cell_id}/report.md`
- `runs/{term_id}/verdicts.json`
- `runs/{term_id}/tool_calls.jsonl`
- `runs/{term_id}/report.md`

### `runs/{cell_id}/verdicts.json`
### `runs/{term_id}/verdicts.json`

Use this shape:

```json
{
"cell_id": "CL_4033094",
"term_id": "CL:4033094",
"name": "...",
"targets_processed": [
Expand Down Expand Up @@ -360,11 +360,11 @@ Notes:
`final_verdict` is `fail` or `uncertain`.
- Term-level pass/fail is driven only by `core` assertions.

### `runs/{cell_id}/tool_calls.jsonl`
### `runs/{term_id}/tool_calls.jsonl`

One JSON object per line, one line per tool invocation.

### `runs/{cell_id}/report.md`
### `runs/{term_id}/report.md`

Short human-readable summary:

Expand Down
91 changes: 77 additions & 14 deletions clara_workflow/stage1/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,53 @@ def _git_show(repo: Path, ref: str, path: str, out: Path) -> None:
)


def _robot_diff(left: Path, right: Path, out: Path, robot: str = "robot") -> None:
subprocess.run(
[
robot, "diff",
"--left", str(left),
"--right", str(right),
"--format", "markdown",
"--labels", "true",
"--output", str(out),
],
check=True,
)
def _robot_diff(
left: Path,
right: Path,
out: Path,
catalog: Path | None = None,
robot: str = "robot",
) -> None:
cmd = [robot, "diff", "--left", str(left)]
if catalog is not None:
cmd += ["--left-catalog", str(catalog)]
cmd += ["--right", str(right)]
if catalog is not None:
cmd += ["--right-catalog", str(catalog)]
cmd += ["--format", "markdown", "--labels", "true", "--output", str(out)]
subprocess.run(cmd, check=True)


# `definition_refs()` scans for the compact `obo:`/`oboInOwl:` curie form (it
# has to — that's what real edit files use), so a conversion must declare the
# same prefixes ROBOT doesn't add by default.
_OFN_PREFIXES = (
"obo: http://purl.obolibrary.org/obo/",
"oboInOwl: http://www.geneontology.org/formats/oboInOwl#",
)


def _robot_convert_to_ofn(
input_path: Path,
output_path: Path,
catalog: Path | None = None,
robot: str = "robot",
) -> None:
"""Convert `input_path` to OWL functional syntax, in the curie form
`definition_refs()` expects, regardless of the input's own format.

This is what lets `definition_refs()` work against an OBO edit file: it
only understands functional-syntax `AnnotationAssertion(...)` lines, so
non-OWL edit files (e.g. `uberon-edit.obo`) need converting first.
"""
cmd = [robot, "convert", "-i", str(input_path)]
if catalog is not None:
cmd += ["--catalog", str(catalog)]
cmd += ["-f", "ofn"]
for prefix in _OFN_PREFIXES:
cmd += ["--add-prefix", prefix]
cmd += ["-o", str(output_path)]
subprocess.run(cmd, check=True)


def _change_to_dict(c: Change) -> dict:
Expand All @@ -68,10 +103,23 @@ def extract(
left_ref: str,
right_ref: str,
edit_file: str,
catalog: Path | None = None,
robot: str = "robot",
) -> tuple[list[Change], dict[str, list[str]]]:
"""Resolve two refs against `edit_file`, run robot diff, parse.

`catalog` is an ODK-style `catalog-v001.xml` (same file used for both
refs, since the import graph practically never changes within a single
reviewed PR): it lets ROBOT resolve every `owl:imports` from the locally
committed files it maps to, rather than fetching each one over the
network. That's what makes this work at all for an edit file whose
imports include a moved/broken PURL, and it also preserves label
resolution for any entity defined only in an import (e.g. BFO's
`part of`, or a cross-referenced UBERON/CL class) -- label text that
`--labels true` can't produce if those imports go unresolved. The caller
(the GitHub Action) is expected to pass the repo's own catalog file;
`robot` falls back to its normal (network) import resolution if omitted.

Returns the parsed changes plus the head-state definition refs per term,
which is where a logical definition's justification lives when the text
definition itself wasn't touched by the PR.
Expand All @@ -80,13 +128,17 @@ def extract(
tdp = Path(td)
left = tdp / "left.owl"
right = tdp / "right.owl"
right_ofn = tdp / "right.ofn"
diff_md = tdp / "diff.md"
_git_show(repo, left_ref, edit_file, left)
_git_show(repo, right_ref, edit_file, right)
_robot_diff(left, right, diff_md, robot=robot)
_robot_diff(left, right, diff_md, catalog=catalog, robot=robot)
# Read definition refs off a functional-syntax conversion rather than
# the right file directly, so this also works for non-OWL edit files.
_robot_convert_to_ofn(right, right_ofn, catalog=catalog, robot=robot)
return (
parse_diff_markdown(diff_md.read_text()),
definition_refs(right.read_text()),
definition_refs(right_ofn.read_text()),
)


Expand All @@ -96,6 +148,16 @@ def main(argv: list[str] | None = None) -> int:
p.add_argument("--left", required=True, help="base git ref")
p.add_argument("--right", required=True, help="head git ref")
p.add_argument("--edit-file", default="src/ontology/cl-edit.owl")
p.add_argument(
"--catalog",
type=Path,
default=None,
help=(
"ODK-style catalog-v001.xml, used for both refs, so ROBOT resolves "
"owl:imports from the locally-committed files it maps to instead of "
"the network. Omit to fall back to ROBOT's normal import resolution."
),
)
p.add_argument("--output", type=Path, required=True)
p.add_argument("--robot", default=os.environ.get("ROBOT", "robot"))
args = p.parse_args(argv)
Expand All @@ -109,6 +171,7 @@ def main(argv: list[str] | None = None) -> int:
left_ref=args.left,
right_ref=args.right,
edit_file=args.edit_file,
catalog=args.catalog,
robot=args.robot,
)
reviewable = reviewable_changes(changes)
Expand Down