Skip to content

🚸 Enable linting scripts to check whether they produce complete data lineage - #3846

Open
sheetalgiri wants to merge 37 commits into
mainfrom
verify_lineage_when_using_agents
Open

🚸 Enable linting scripts to check whether they produce complete data lineage#3846
sheetalgiri wants to merge 37 commits into
mainfrom
verify_lineage_when_using_agents

Conversation

@sheetalgiri

@sheetalgiri sheetalgiri commented Aug 6, 2026

Copy link
Copy Markdown
Contributor
  • Added verify_lineage() function that parses a Python script with ast and checks:

    • lineage tracking calls (ln.track(...) / ln.finish(...) or imported equivalents)
    • Finds lines in the code that contain a file/folder path/URL and track them, and check if these these are eventually used in a LaminDB call to verify lineage tracking
  • Updated skills.md so that for every python script that the agent creates/plans to run, it verifies the lineage by calling the function and doing assertions on the verification results, make sure everything passes. If there is a failure, it goes back and makes modifications to the script and runs the function again.

  • Update skills.md to create artifacts from in-memory objects when possible using from_dataframe(), from_anndata(),etc instead of saving it to disk and doing save()

Why:

  • Agents can be unreliable when it comes to doing the lineage tracking properly

Limitations:

  • Only supports python scripts

@codecov

codecov Bot commented Aug 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.66667% with 19 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.04%. Comparing base (112244e) to head (be32063).
⚠️ Report is 9 commits behind head on main.

Files with missing lines Patch % Lines
lamindb/core/_verify_lineage.py 91.62% 19 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #3846      +/-   ##
==========================================
- Coverage   91.55%   87.04%   -4.52%     
==========================================
  Files          88       89       +1     
  Lines       15747    16066     +319     
==========================================
- Hits        14417    13984     -433     
- Misses       1330     2082     +752     
Files with missing lines Coverage Δ
lamindb/core/__init__.py 62.50% <100.00%> (-11.42%) ⬇️
lamindb/core/_context.py 89.41% <ø> (ø)
lamindb/core/_verify_lineage.py 91.62% <91.62%> (ø)

... and 20 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

Deployment URL: https://6a5bda96.lamindb.pages.dev

sheetalgiri and others added 20 commits August 11, 2026 10:42
…d checks:

- lineage tracking calls (ln.track(...) / ln.finish(...) or imported equivalents)
- LaminDB input retrieval patterns (e.g. ln.Artifact.get(...), ln.Collection.filter(...), etc.)
- LaminDB output persistence patterns (.save() / ln.save(...))
- suspicious non-LaminDB input reads (e.g. pd.read_*, open, np.load, etc.)
- Instead of having checks like has_lamindb_inputs and has_lamindb_outputs, use checks has_external_inputs and has_external_outputs. in this case even if there is no input/output a script can be correct
- if there are external inputs/outputs printing those function calls, to help guide the agent in what it should fix
lamin-cli: points to main (#254 merged).
lamin-skills: points to the latest commit on sheetalgiri-patch-1
(includes the --session-id finish fix on top of verify_lineage).
lamin-cli: points to main (#255 merged).
lamin-skills: points to the latest commit on the still-open
sheetalgiri-patch-1 branch.
- checks for file names in a script like 'x.txt'or 's.npy'or anything and whether or not they are associated with a lamin call or not , regardless of what it is, directly or indirectly, if all file names have this kind of association the function passes the script
@sheetalgiri
sheetalgiri requested a review from falexwolf August 13, 2026 08:41
@falexwolf falexwolf changed the title 🚸 Verify lineage for scripts created by agents using a deterministic function 🚸 Enable linting scripts to check whether they produce complete data lineage Aug 13, 2026
`ln.track()` and `ln.finish()`.
- Extracts path-like strings from literals, variables, call arguments,
and simple path joins (string `+` or path `/` operations).
- Classifies path usage as tracked when it appears in LaminDB calls

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see quite a lot of logic related to this. Why not just flag paths that are not going through lamindb? Wouldn't this enable us to eliminate a lot of the code?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was my initial idea but in practice I saw agents using non-lamindb write functions in combination with lamindb save. Its also inevitable in general because we would have to do this for file formats other than those where we have ln.Artifact.from_* methods like from_dataframe or from_anndata

and simple path joins (string `+` or path `/` operations).
- Classifies path usage as tracked when it appears in LaminDB calls
(`ln.*`, `lamindb.*`, including `ln.Artifact(...).save()` patterns),
and flags paths that only appear in non-LaminDB calls.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the important thing.

assert result.missing_lineage == ()


def test_verify_lineage_positive_output_from_dataframe(tmp_path: Path):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would combine this test and the previous one through parametrization. Both tests are very simple baseline tests.

assert result.missing_lineage == ()


def test_verify_lineage_imported_symbols_match_current_behavior(tmp_path: Path):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the reason for testing this false positive behavior? 🤔

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a mistake, it should be tracked

assert "Missing ln.finish() call in script." in result.missing_lineage


def test_verify_lineage_positive_zero_io_script(tmp_path: Path):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would merge this into tests 1 & 2 through parametrization as the structure is the same

assert result.missing_lineage == ()


def test_verify_lineage_negative_missing_lineage_tracking_calls(tmp_path: Path):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about @ln.flow()? If I had a script that used @ln.flow() instead of ln.track() and ln.finish(), would that be also a false negative (meaning negative is "not tracked").

assert "Missing ln.finish() call in script." in result.missing_lineage


def test_verify_lineage_negative_external_input_even_when_script_has_lamindb_io(tmp_path: Path):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great test! I think creating a catalog of these negative examples and then running a parametrized test against them is the most important

What matters is to detect scripts that don't produce lineage with high confidence

assert any("./local_input.csv" in item for item in result.missing_lineage)


def test_verify_lineage_negative_external_output_write(tmp_path: Path):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great!

assert any("./local_output.txt" in item for item in result.missing_lineage)


def test_verify_lineage_negative_open_read_untracked(tmp_path: Path):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great!

assert any("./local_input.txt" in item for item in result.missing_lineage)


def test_verify_lineage_positive_local_write_then_lamindb_save(tmp_path: Path):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's cool that this is properly classified. Impressive!

@falexwolf

Copy link
Copy Markdown
Member

Cool! 😄

(1) What about scripts that track lineage via @ln.flow()?
(2) I think I understand now why we care about classifying positive usage of ln.Artifact() etc.: because one needs to detect whether an invalid call writes a temp file that's later ingested, right?
(3) I would use parametrized tests a bit more heavily since this hinges so much on being a catalog of scripts with their error types
(4) What about docs and public API? Is this supposed to be a public API or do we want to first use this internally only? If it's a public API we should add a screenshot of the docs and a nice example. ☺️

@sheetalgiri

sheetalgiri commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

@falexwolf thanks for the thorough feedback !

(1) What about scripts that track lineage via @ln.flow()?

This is something I missed, will add it

(2) I think I understand now why we care about classifying positive usage of ln.Artifact() etc.: because one needs to detect whether an invalid call writes a temp file that's later ingested, right?

Exactly, when the path is written to a temp file it adds it to the 'untracked' list. Once it sees that the same path is saved on Lamindb, it adds it to the 'tracked' list and then its considered resolved.

(3) I would use parametrized tests a bit more heavily since this hinges so much on being a catalog of scripts with their error types

Makes sense 👍

(4) What about docs and public API? Is this supposed to be a public API or do we want to first use this internally only? If it's a public API we should add a screenshot of the docs and a nice example. ☺️

I would say we use it internally first and get more feedback/experience using it with agents, what do you think?

@falexwolf

Copy link
Copy Markdown
Member

I would say we use it internally first and get more feedback/experience using it with agents, what do you think?

Agreed!

@sheetalgiri

Copy link
Copy Markdown
Contributor Author

@falexwolf I made the changes, could you please have another look?

@sheetalgiri

sheetalgiri commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

While testing this, I noticed that our skills are kind of bad at using ln.flow(). There is no mention of using it in the SKILLS.md and then it also uses it incorrectly. It generated:

import lamindb as ln

# 1. Start global script tracking
ln.track()

# 2. Use @ln.flow() for functions nested inside an ln.track() session
@ln.flow()
def process_sample(sample_id: str):
    df = ln.examples.datasets.mini_immuno.get_dataset1()
    return ln.Artifact.from_dataframe(df, key=f"processed_{sample_id}.parquet").save()

# 3. Call the step function
artifact = process_sample(sample_id="batch_01")

# 4. Finish the global tracking session
ln.finish()

which caused the error

RuntimeError: Please use @ln.step() or clear the global run context before using @ln.flow(): no `ln.track()` or `@ln.flow(global_run='clear')`

So, ln.flow() should never be used inside ln.track(). Is that correct @falexwolf ?

I could add the skills for this but I would need to understand better when exactly we want agents to use ln.flow(), is it something meant to be used for notebooks and not for normal scripts?

FYI @Ebad371

@falexwolf

Copy link
Copy Markdown
Member

So, ln.flow() should never be used inside ln.track(). Is that correct @falexwolf ?

Yes, that's correct.

While ln.flow() is ultimately the more powerful and better pattern, we can also be pragmatic for now and just focus on ln.track() and ln.finish(). We can push out dealing with @ln.flow() in the upcoming weeks, when the need arises. I use it in most of my work, so I'll run into this sooner or later.

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.

3 participants