Skip to content

feat: add enrich step that records agent-authored insights on the discover inventory - #95

Open
matthewmoorcroft wants to merge 59 commits into
stack/adf-discoveryfrom
stack/agentic-insights
Open

matthewmoorcroft wants to merge 59 commits into
stack/adf-discoveryfrom
stack/agentic-insights

Conversation

@matthewmoorcroft

@matthewmoorcroft matthewmoorcroft commented Sep 16, 2026 •

Copy link
Copy Markdown
Member

Intent

Add an enrich step to discovery, run after discover. An agent authors insights about the factory. Enrich checks them against the inventory and records them in metadata/agentic_insights.json. The library then rebuilds inventory.json by itself, from source_graphs.json plus that document under one added insights key, so a later step reads the source graph and the enrichment from one file. flowx itself runs no LLM: the agent authors the insights and passes nothing else, and the library validates them, records them and builds the inventory.

What changed

  • models/insights.py, discovery_insights.py: the insights schema and its validator.
    • Insights cover the overview, the system recommendation, per-pipeline insights and pipeline relationships.
    • Every pipeline name must be a listed inventory pipeline, with at most one insight per pipeline. The one exception: a control relationship may name a pipeline discover left unlisted (filtered out with --pipeline, or with no activities) when its edge is a resolved control edge in the inventory.
    • A control relationship must match a real (from, to, via_task_key) lineage edge marked resolved: true. An inferred relationship must carry evidence and a confidence level.
    • Recommended patterns have a count cap, and a simplification pattern must declare its release_state.
    • Authored copies of library-owned keys are rejected.
    • authored_against (the source_graphs_sha256 the agent read) is optional. When given it must match, so insights written before discover ran again are refused; when absent, the library stamps the hash itself. The skill passes it by default.
    • When the inventory records a source_graphs_sha256, the saved source_graphs.json must still be a usable object matching that hash, its source must equal the inventory's source, and source_dir must be a string. With source_graphs.json saved, an inventory missing its hash is refused.
    • Only string names and edge fields count, and a malformed inventory shape is treated as unknown rather than crashing.
    • Every violation is reported in one pass, and nothing is written.
  • Recording:
    • agentic_insights.json is stamped with the schema version and with inventory, source-graph and insights (agentic_insights_sha256) hashes. authored_against is checked but not stored.
    • When the inventory records a graphs hash, its deterministic part is projected again from source_graphs.json with the shared build_source_inventory (project_inventory). Only source_dir and the hash come from inventory.json, plus a per-source rule for listing empty pipelines. The result is byte-identical to what discover writes, and the insights are validated against it. Inventories without a graphs hash (Airflow today) keep reusing their existing deterministic part.
    • Re-running with the same insights rewrites the same bytes.
    • discovery_serde.canonical_sha256 is now public so the two modules share one hash.
  • Writing both files:
    • Only one enrich writes an output directory at a time. It holds metadata/.enrich.lock from reading the inventory until both files are replaced, and a second call made meanwhile gets a violation and writes nothing.
    • Discover does not wait for that lock; it clears metadata/, the lock with it. So before writing its temp files and again right before the replaces, enrich checks that its own lock and the inventory.json bytes it read are unchanged; if discover ran, it writes nothing and reports "discover ran during this enrich; prepare and enrich again".
    • If replacing the inventory fails, the insights file is put back, by an atomic replace, to the block the unchanged inventory holds, so the live file is never half-written and the pair always matches.
    • Two gaps remain. First, a process killed or interrupted between the two replaces, or a rollback that itself fails, can leave agentic_insights.json one write ahead of inventory.json. The lock then stays behind, so the next enrich refuses. Locally, delete metadata/.enrich.lock and run enrich again; on the hosted MCP server, re-run discover (which clears metadata/ and the lock), then prepare and enrich again. Second, a discover plus two or more enrich runs on one output folder within a few milliseconds can slip past the checks; because the temp file names are shared, one such case can leave a stale inventory.json while enrich reports ok: true. Sequential use is safe. Both gaps are stated in the module docstring and the skill, and per-run temp names are a tracked follow-up.
  • Entry points: the adapter enrich subcommand (--output-dir, --insights-path, --out) and the MCP enrich command. On MCP, action: "prepare" returns metadata/inventory.json for a hosted agent that can't read the server's output_dir. It goes through the same size-bounded inline or volume/workspace delivery package uses for bundles. An inventory too large to return inline makes prepare fail with ok: false, naming the upload options. The default action: "apply" records insights given inline or as a path.
  • Docs: the flowx-enrich skill (SKILL.md, plus insights.md for the schema). It is registered in plugin.json, the README and the install docs. flowx-discover continues into it unless a deterministic-only run was requested; the one-shot MCP migrate command runs without it.
  • Carried from refactor: add source-neutral discovery graph contract and shared lineage core #84 and feat(adf): save source graphs with dataset lineage at ADF discover #94: feat(adf): save source graphs with dataset lineage at ADF discover #94's final head 337d2d6, which includes refactor: add source-neutral discovery graph contract and shared lineage core #84's final head, is merged in (da8f83a, then a45bb58); the conflicts were all in feat(adf): save source graphs with dataset lineage at ADF discover #94-owned ADF files and were resolved to feat(adf): save source graphs with dataset lineage at ADF discover #94's side. That history includes 880c202 and its revert 8e7d302, so uv.lock is unchanged.

How it was tested

  • Unit tests cover the validator, the recording, the paired write and its failure paths, the CLI and the MCP command. The full unit suite (1900 passed; 1927 with the mcp extra, which CI does not install), ruff and mypy pass.
  • ADF discover, convert and package on the fixture pipelines: output is byte-identical to feat(adf): save source graphs with dataset lineage at ADF discover #94's head 337d2d6, and Airflow output is unchanged. On those fixtures, and on an export with an empty pipeline, the inventory rebuilt from source_graphs.json is byte-identical to discover's, and enrich adds only the insights key.
  • Driven live: discover, enrich, convert and package through the adapter CLI on the ADF fixtures and an Airflow DAG, and discover and enrich through the MCP stdio server.
  • Codex cross-review, read-only, six times, plus one review of the carried merge. Every fix has a regression test that fails on the previous code.
    • Round 1 found two feat: add enrich step that records agent-authored insights on the discover inventory #95 issues, both fixed: insights could bind to a newer discover (now authored_against), and a failed write could leave the two files disagreeing (now a paired write with rollback).
    • Round 2 found three, all fixed in 278b57e, with 8fa9bac from the no-mistakes review:
      1. Concurrent enrich calls could interleave their writes. Writes now happen under the lock, and the crash window is stated as above.
      2. Duplicate insights for one pipeline passed validation. They are now a violation.
      3. A source_graphs.json that is not a JSON object crashed enrich. It is now a "not usable" violation. The review also found that malformed graph entries in an unhashed file still crashed enrich, so the recorded hash is now compared before any graph is read.
    • Round 3 found one, fixed in 3a8dd03: a failed inventory replace rolled the insights file back by truncating it in place, so a second disk error left it partial and the lock was dropped. It is now restored by atomic replace, and the lock is kept if the rollback fails. The no-mistakes review then found that a Ctrl-C landing just after a successful rename was rolled back as if the rename had failed; fb9b933 rolls back only on OSError and keeps the lock for an interrupt between the two replaces.
    • Round 4 found two, both fixed: a failed recovery run could roll the insights file back to a copy already ahead of the inventory (5d86088), and a hosted MCP agent had no way to read the inventory it authors against (ffe7229, the prepare action, narrowed to inventory.json in 7494460 so large ARM files can't crowd it out of the size bound).
    • Round 5 found one, fixed in 2fb4355: a whitespace-only edge_identity passed validation; it is now stripped before the non-empty check.
    • Round 6 applied the reviewer checklist from refactor: add source-neutral discovery graph contract and shared lineage core #84 and feat(adf): save source graphs with dataset lineage at ADF discover #94 to the full diff, alongside a manual pass. Six findings, fixed in 3f4c456: an inventory with its hash deleted was trusted, the unhashed source and source_dir drove the rebuild, numeric names were stringified into valid names, an unresolved control edge could be annotated, malformed inventory shapes crashed enrich, and the skill did not say the one-shot migrate skips enrich. The no-mistakes review then found that a resolved control edge into an unlisted pipeline could not be annotated, although the skill says it can; fixed in a354544.
    • Greg's round-3 review raised two points, both addressed: prepare reported success when the inventory was dropped by the inline size bound (750037e, 8951cc3), and a discover during an enrich could let old insights overwrite the new inventory (8951cc3; the remaining windows are documented in 117260a, 0fc77bf and 6ed0b4b). A process-id check in discover was tried first and replaced, because it could not tell a stale lock from a live one.
    • The review of the carried merge da8f83a, with the same checklist, found nothing: the resolution keeps every refactor: add source-neutral discovery graph contract and shared lineage core #84 and feat(adf): save source graphs with dataset lineage at ADF discover #94 change, and enrich agrees with the stricter carried rules, including the byte-identical rebuild.
  • Aligned with the 8 Oct design note in 9a0ae6b: the rebuild from source_graphs.json, optional authored_against, and the rename from source_insights.json to agentic_insights.json. Each has a test that fails without it.

Merge order

Merge #84 first, then #94 retargeted to main, then this PR (retargeted to main once #94 has merged), then #96 and #97.

The enrich and discover docs point to the flowx-route skill, which lands with #97.

This pull request and its description were written by Isaac.

matthewmoorcroft and others added 2 commits September 16, 2026 17:25
Add a source-neutral insights artifact (models/insights.py) and an
enrich_inventory pass (discovery_insights.py) that layers agentic insights
onto the unified inventory, with a validator that forbids inferred-only keys
by presence and aggregates per-edge errors. Expose it through the discover
CLI and MCP (enrich subcommand) and document it in the flowx-discover skill.

Consolidates PR #82 (agentic insights enrich_inventory + validator + CLI/MCP).

Co-authored-by: Isaac <no-reply@databricks.com>
Introduces the first-class flowx-enrich skill (SKILL.md + insights.md,
moved here from flowx-discover) and reframes flowx-discover to chain into
enrich by default while keeping a deterministic-only skip path. Registers
flowx-enrich in the plugin manifest.

The agent authors the insights JSON; the library only validates and merges
it (no LLM in the library). Skill/manifest docs only; no src/ changes.

Co-authored-by: Isaac <no-reply@databricks.com>
matthewmoorcroft added a commit that referenced this pull request Sep 17, 2026
Propagate the flowx-enrich skill (from #95) up the stack ahead of adding
the routing skill changes on this branch.

Co-authored-by: Isaac <no-reply@databricks.com>
matthewmoorcroft and others added 7 commits September 18, 2026 11:32
FIX 4: The insights validator only accepts edge_type 'control'/'inferred'.
Improve the rejection message so a mis-typed cross-pipeline data coupling (e.g.
edge_type 'data') is steered onto the 'inferred' tier with the required
'evidence' + 'confidence', instead of just being rejected. Add an explicit
"data coupling -> inferred" worked example (wrong vs. right) to the enrich
insights.md reference, and strengthen the regression test to assert the guidance.

FIX 8a: Port the GA/Preview grounding guidance that previously lived only in the
Airflow skill into flowx-enrich. Before recommending a connector or Lakeflow
Connect pattern, the authoring agent must verify the feature's current
GA-vs-Preview status AND target-workspace availability against public Databricks
docs and cite the source; Private Preview is treated as doNotSuggest absent
confirmed enrollment. Prose/guidance only.

Co-authored-by: Isaac <no-reply@databricks.com>
Propagate the discover loud-0-pipeline warning + activity-count units label up
the stack.

Co-authored-by: Isaac <no-reply@databricks.com>
Cross-review BLOCKING 2: the FIX 8a prose contradicted itself -- it required
verify-before-recommend but then permitted a conditional recommendation when
release state is uncertain, and it required citing the source but not recording
the resolved status.

Fix: remove the conditional-recommendation escape hatch. The agent MUST verify
each recommended capability/connector's GA-vs-Preview status AND workspace
availability against current public Databricks docs BEFORE recommending it, and
MUST record the resolved GA/Preview status (and cite the doc source) in the
pattern's fit / conversion_notes. If it cannot verify, it must not recommend the
capability and should offer a verified alternative instead. Consistent with the
Airflow lakeflow-connect grounding it was ported from. Prose only; references
only real insight fields (fit / conversion_notes / recommended_patterns).

Co-authored-by: Isaac <no-reply@databricks.com>
…insights

Propagate the --adf-source-path warning correction up the stack.

Co-authored-by: Isaac <no-reply@databricks.com>
…mended patterns

Turn the enrich GA-grounding rule from prose into a real, validated, structured
field. RecommendedPattern gains optional release_state
("ga"/"public_preview"/"private_preview"/"beta"/"unknown") + release_state_source
(the doc citation). The insights validator now:

- enum-validates release_state when present;
- REQUIRES release_state when simplification_pattern is true (intended new
  strictness -- a distinctive capability must declare its verified release state);
- REQUIRES a non-empty release_state_source for public_preview/private_preview/beta
  (not for ga/unknown);
- aggregates all violations (no fail-fast), like the rest of the module.

The enrich skill prose now sets the structured fields instead of burying status in
fit/conversion_notes, and documents the tiered surfacing intent: ga = no warning;
public_preview = informational disclosure (production-ready per Databricks, confirm
availability); private_preview & beta = prominent warning; unknown = could not
verify, prefer a verified alternative. The verify-before-recommend /
never-recommend-now-verify-later rule is kept.

Fields are optional/back-compat; only the simplification-requires-release_state rule
is a stricter check.

Co-authored-by: Isaac <no-reply@databricks.com>
Revise the release-state surfacing to pure factual disclosure per design review;
the validator/grounding rules are unchanged (release_state still required when
simplification_pattern is true; release_state_source still required for
public_preview/private_preview/beta). Only how the state is *surfaced* changes:

- ga and unknown are silent (unknown is treated exactly like ga);
- public_preview is disclosed as "Public Preview (production-ready)";
- private_preview and beta are stated as plain factual labels.

Drop the prominent-warning / not-production-ready / gated framing from the skill
prose and the RecommendedPattern docstring. The separate recommendation-eligibility
rule (treat Private Preview as doNotSuggest without confirmed enrollment) and the
verify-before-recommend grounding rule are kept as-is.

Co-authored-by: Isaac <no-reply@databricks.com>
Follow the neutral-disclosure design through the RELEASE_STATES constant comment
(state definitions are now purely factual; surfacing is described on
RecommendedPattern.release_state) and the validator docstring ("disclose" rather
than "warn"). No behavior change.

Co-authored-by: Isaac <no-reply@databricks.com>
matthewmoorcroft and others added 3 commits September 18, 2026 17:06
…rop hardcoded release_state examples

Private Preview features are no longer hard-gated as `doNotSuggest`.
Instead the flowx-enrich skill now says to disclose the verified release
state via the structured release_state / release_state_source fields, note
that Private Preview typically needs workspace enrollment/entitlement, and
let the customer decide whether to proceed — the tool does not unilaterally
refuse to suggest it. The requirement to verify the release state against
current public docs before recommending is kept, and so is the neutral
(non-warning) surfacing. The release_state mechanism and validator rules
are unchanged.

Also neutralize the illustrative JSON examples so no real connector is
pinned to a literal GA/Preview status (release state goes stale): the
system_recommendation and pipeline_insights examples now use anonymized
pattern names with a clearly-placeholder release_state value that tells the
author to verify live and not copy it. No real-connector-to-status literal
remains in the enrich skill. The Airflow-side grounding rule is tracked
separately (#103) and is untouched here.

Co-authored-by: Isaac <no-reply@databricks.com>
… graph-contract naming) into stack/agentic-insights

Co-authored-by: Isaac <no-reply@databricks.com>
… from it

Enrich used to merge the insights block straight into inventory.json. It now
writes metadata/source_insights.json with the library-owned schema_version,
inventory fingerprint, the source_graphs.json hash the inventory records,
and its own content hash, then rebuilds inventory.json from discover's
deterministic projection plus that document, so the inventory's insights
block is always a copy of the file. Enrich refuses an inventory that no
longer matches the saved source graphs, and authored insights cannot set
the library-owned hashes. merge_into_inventory and the CLI and MCP surfaces
keep working as before.

Co-authored-by: Isaac <no-reply@databricks.com>
@matthewmoorcroft matthewmoorcroft changed the title Phase 1 stack #3: agentic insights / enrich Phase 1 stack #3: agentic insights in source_insights.json, inventory rebuilt from discovery plus insights Oct 5, 2026
matthewmoorcroft and others added 6 commits October 5, 2026 21:06
…ights

Co-authored-by: Isaac <no-reply@databricks.com>
Wording only. The enrich and discover skills, the enrich CLI help and
docstring, the MCP enrich surface and the insights module docstrings still
said the library merges insights into inventory.json. They now say it
validates them, records them in source_insights.json and rebuilds the
inventory, and the insights model docstring uses the source-neutral
discovery graph contract name. The enrich example names its authored input
authored_insights.json so it is not mistaken for the library's output.

Co-authored-by: Isaac <no-reply@databricks.com>
…ntic-insights

Carries the #94 review fixes forward: ADF dataset identities qualified by file name
and literal server, parameterised parts never form an identity, the shared lineage
parser in ir_serde, and the source_graphs.json docs. The flowx-discover output row
for source_graphs.json conflicted with this branch's wording commit; the resolution
keeps both wordings.

Co-authored-by: Isaac <no-reply@databricks.com>
@matthewmoorcroft matthewmoorcroft changed the title Phase 1 stack #3: agentic insights in source_insights.json, inventory rebuilt from discovery plus insights feat: add enrich phase that validates and records agent-authored discovery insights Oct 6, 2026
matthewmoorcroft added a commit that referenced this pull request Oct 6, 2026
…agentic-component

Carries #94 and #95 forward: dataset identity fixes, the shared lineage parser, one
shared canonical hash (discovery_serde.canonical_sha256), hardened insights validation,
and the enrich wording and docs.

Co-authored-by: Isaac <no-reply@databricks.com>
matthewmoorcroft added a commit that referenced this pull request Oct 7, 2026
…/routing-agentic

Carries #94, #95 and #96 forward: dataset identity fixes, the shared lineage parser,
one shared canonical hash (discovery_serde.canonical_sha256), hardened insights
validation and enrich wording, and the agentic component resource-key checks.

Conflicts kept both wordings: the AGENTS.md discover line matches phase1-integration,
and the MCP tool description takes #95's corrected enrich text with this branch's
route and fill_agentic entries. README listed source_insights.json twice after the
auto-merge; the two lines are combined into one that keeps both descriptions.

Co-authored-by: Isaac <no-reply@databricks.com>
matthewmoorcroft added a commit that referenced this pull request Oct 7, 2026
…integration

Brings in the final heads of #95 (5fa3ea2), #96 (b508ecc) and #97 (7cc139f), so the
integration branch carries every Phase 1 PR as validated: insights hardening and the
shared canonical hash, the agentic component checks, and the routing replay model.

Co-authored-by: Isaac <no-reply@databricks.com>
…les together

Authored insights now carry authored_against, the source_graphs_sha256 the author read from
inventory.json. Enrich refuses a missing or different value, so insights written before
discover ran again never bind to the newer inventory; inventories without a recorded graph
hash need none. source_insights.json and inventory.json are now replaced together: both
temp files are written first, and if replacing the inventory fails the previous insights
are restored. The enrich insights guide documents the new field. Each fix has a
regression test that fails on the previous code.

Co-authored-by: Isaac <no-reply@databricks.com>
matthewmoorcroft and others added 3 commits October 8, 2026 17:49
Fixes the Codex round-5 finding. The edge_identity check accepted a string of
only spaces because it tested truthiness, and an inferred edge's identity is
never resolved afterwards, so the blank identity was recorded. It is now
stripped before the check, as evidence and recommended-pattern text already
are.

Co-authored-by: Isaac <no-reply@databricks.com>
matthewmoorcroft added a commit that referenced this pull request Oct 8, 2026
…agentic-component

Co-authored-by: Isaac <no-reply@databricks.com>
matthewmoorcroft added a commit that referenced this pull request Oct 8, 2026
…tic-component

Co-authored-by: Isaac <no-reply@databricks.com>
matthewmoorcroft added a commit that referenced this pull request Oct 9, 2026
… into stack/routing-agentic

Conflicts kept both parents' behaviour:
- ir_serde: #97's _locate_task split plus #96's placeholder-owned fields for an
  AgenticComponentActivity replacement.
- mcp/server.py, app/README.md: #96's enrich prepare/apply text plus #97's
  route and fill_agentic entries.
- README.md, architecture.mdx: #96's wording.

#95's rename reaches #97's own code: the plan, the route audit, routing and
their tests now bind to agentic_insights.json and its agentic_insights_sha256
self-hash key (routing read the old key, so after the merge every plan would
have recorded a null insights hash).

Co-authored-by: Isaac <no-reply@databricks.com>
matthewmoorcroft and others added 11 commits October 15, 2026 12:17
…nto stack/agentic-insights

# Conflicts:
#	src/flowx/discovery_serde.py
#	src/flowx/lineage.py
…dges

Addresses the four review points on the ADF discovery lineage:

- A linked service name must be a plain name before it stands in for a
  hidden server or account, so an unevaluated [parameters('ls')] never
  becomes part of a table identity or a store qualifier.
- The file store reads S3 / GCS bucketName alongside fileSystem and
  container, so two buckets with the same folder layout get different
  signatures and no longer link.
- build_graph_lineage takes the names of the pipelines in the export, and
  an ExecutePipeline edge is resolved only when its child is one of them.
  The IR build_control_edges / build_lineage accept the same names.
- A test runs discover twice on the same export and checks the
  inventory.json bytes match and carry no generated_at.

Same defect found in the sweep: a call-site folderPath that is an
unevaluated ARM expression was accepted as the weak path signature's
literal anchor; it is now no anchor.

Co-authored-by: Isaac <no-reply@databricks.com>
(cherry picked from commit 1215a8b)
… file names in signatures

(cherry picked from commit 69c1762)
…der anchors holding @

Cross-review follow-ups to the review fixes:

- ADF compares pipeline names case-insensitively, so the ADF mapper now
  resolves an ExecutePipeline child against the exported pipeline names
  that way. The shared control-edge core stays exact-match, and the
  edge keeps its target as written.
- The weak path signature rejects an unevaluated ARM expression or a
  {{ placeholder as its folder anchor before splitting on @, so a quoted
  @ inside an ARM expression no longer slips through. ADF @ expressions
  still reduce to their shape.

Co-authored-by: Isaac <no-reply@databricks.com>
(cherry picked from commit 6b0ea5d)
…r ARM file names

A location that names no store of its own (SFTP, file server, FTP, Azure
Files, HDFS, HTTP) now qualifies its path signature with the plain,
unparameterised linked service name, and gets no signature when that name
is not plain or the linked service takes parameters. An unevaluated ARM
fileName holding a quoted @ is kept verbatim instead of being reduced to an
ADF expression shape, so two different ARM file names never join.

Co-authored-by: Isaac <no-reply@databricks.com>
(cherry picked from commit 4f7d2aa)
Applies the Greg-style review checklist to #95's full diff:
- An inventory missing source_graphs_sha256 while source_graphs.json is saved
  is refused instead of trusted (C1).
- The unhashed source and source_dir the rebuild takes from inventory.json
  must be the graphs' own source and a string (C4, G1).
- Only string pipeline names and string control-edge fields count, and only
  edges discover marked resolved can be annotated (B4, B1).
- Malformed pipelines, lineage or control_edges are treated as unknown
  rather than crashing (D1).
- The enrich skill states that one-shot MCP migrate records no insights and
  that only resolved control edges can be annotated (E4).

Co-authored-by: Isaac <no-reply@databricks.com>
# Conflicts:
#	src/flowx/sources/adf/dataset_lineage.py
#	tests/unit/test_adf_dataset_lineage.py
#	tests/unit/test_adf_discovery_mapping.py
#	tests/unit/test_adf_inventory_superset.py
matthewmoorcroft added a commit that referenced this pull request Oct 19, 2026
Co-authored-by: Isaac <no-reply@databricks.com>

@ghanse ghanse left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The enrich design matches the description, and the validation, hash binding and rollback are well tested. Fix the false success on a truncated inventory before merging.

Comment thread src/flowx/mcp/server.py Outdated
return {"ok": False, "error": f"No inventory.json under {inventory_path.parent}; run discover first."}
with tempfile.TemporaryDirectory(prefix="flowx-inventory-") as temporary:
shutil.copy(inventory_path, temporary)
return {"ok": True, "output_dir": str(output_dir), **_bundle_output(p, Path(temporary))}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Blocking: prepare returns ok: True even when inventory.json was dropped by the 2MB size cap, so the agent gets success with nothing to read. Fail when inventory.json is missing from the bundle (point to the output path instead), and add a test with an oversized inventory.

Comment on lines +667 to +691
@contextmanager
def _enrich_lock(metadata_dir: Path) -> Iterator[bool]:
"""Hold the output directory's enrich lock, yielding ``False`` when another enrich already holds it.

The lock is a file created with ``O_EXCL`` (portable, unlike ``fcntl``) and removed when the
enrich finishes or raises. It is left behind only when the two files may disagree -- a killed
process, or an error that is or was raised from :class:`_UnrecoveredWriteError` (a failed
rollback, or an interrupt between the two replaces) -- so the next enrich refuses until the lock
is removed.
"""
lock_path = metadata_dir / ENRICH_LOCK_FILENAME
try:
os.close(os.open(lock_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY))
except FileExistsError:
yield False
return
keep_lock = False
try:
yield True
except BaseException as error:
keep_lock = isinstance(error, _UnrecoveredWriteError) or isinstance(error.__cause__, _UnrecoveredWriteError)
raise
finally:
if not keep_lock:
lock_path.unlink(missing_ok=True)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This lock only stops two enrich runs from overlapping. discover removes metadata/ (and the lock with it), so a discover during an enrich can let the old insights overwrite the new inventory. Sequential use is fine. The risk is the recovery path that re-runs discover.

matthewmoorcroft and others added 6 commits October 20, 2026 05:50
…nning enrich

Greg's round-3 review of #95:
- MCP enrich prepare returned ok: true when inventory.json was dropped by the
  inline size bound. It now fails and points to output_volume_path /
  output_workspace_path.
- Discover cleared metadata/ (and the enrich lock with it) under a running
  enrich, so that enrich's old insights could overwrite the new inventory.
  The lock now records its holder's process id, and discover refuses while
  that enrich is still running. A lock left by a killed enrich is still
  cleared by discover, so the hosted recovery path is unchanged.

Co-authored-by: Isaac <no-reply@databricks.com>
Replaces the process-id check from 750037e (option B of the round-3 lock
decision). A PID cannot prove ownership after PID reuse, and a check in
discover still raced an enrich that started just after it.

- Discover is unchanged again and still clears any lock, so the hosted
  recovery for a stale lock is untouched and Airflow needs no change.
- The lock holds a random token. Right before writing, enrich checks that its
  own lock is still in place and that inventory.json still holds the bytes it
  read; otherwise it writes nothing and reports "discover ran during this
  enrich; prepare and enrich again". The check runs before the temp files are
  written and again right before the first replace.
- An enrich only removes the lock if it is still its own.
- enrich prepare reports the size of the copied inventory, not the live file.

Co-authored-by: Isaac <no-reply@databricks.com>
Option A of the round-3 residual-race decision: sequential use is safe and a
discover during an enrich is detected around the read and before the write;
the handoff windows between the final check and the replaces, and in lock
release, are documented as not closed.

Co-authored-by: Isaac <no-reply@databricks.com>
matthewmoorcroft added a commit that referenced this pull request Oct 20, 2026
A move only: every function and constant keeps its exact body (checked by
comparing the AST of each definition before and after). dataset_identity
holds the physical identity and the value helpers it shares, dataset_store
the store that qualifies a signature, path_signature the structural
signature, and dataset_lineage puts them together for each read and write.
activity_data_assets and resolve_dataset_identity are still imported from
dataset_lineage, the only names #95 to #97 use.

Co-authored-by: Isaac <no-reply@databricks.com>
@matthewmoorcroft

Copy link
Copy Markdown
Member Author

Thanks Greg, both round-3 comments are addressed.

  • prepare false success (blocking): prepare now returns ok: false when inventory.json is too large to return inline, naming output_volume_path and output_workspace_path, with a test on an oversized inventory (750037e, error size taken from the copied file in 8951cc3).
  • Discover during an enrich: enrich now respects discover rather than discover refusing, because discover is also the hosted recovery for a stale lock and a process-id check could not tell a stale lock from a live one. Under its lock, enrich checks its own lock token and the inventory.json bytes it read before the temp files and again right before the replaces, and if discover ran it writes nothing and reports "discover ran during this enrich; prepare and enrich again"; tests cover a discover after the read and after the temp files (8951cc3). Sequential use is safe. The remaining windows, between the first check and the temp files, between the final check and the replaces, and in releasing the lock, are a few file operations wide, need two or more concurrent enrich runs plus a discover on one output folder, and are not closed; they are stated in the module docstring and the enrich skill (117260a, 0fc77bf, 6ed0b4b). One of them can leave a stale inventory with ok: true, because the temp file names are shared between runs; that one is documented and tracked as a follow-up (per-run temp names).

matthewmoorcroft added a commit that referenced this pull request Oct 20, 2026
Co-authored-by: Isaac <no-reply@databricks.com>
matthewmoorcroft added a commit that referenced this pull request Oct 20, 2026
Brings in #96's round three with #95 6ed0b4b, #94 337d2d6 and #84 5ebdfcb
merged in. No conflicts.

Co-authored-by: Isaac <no-reply@databricks.com>

This branch has not been deployed

No deployments
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.

2 participants