Repository navigation
feat: add enrich step that records agent-authored insights on the discover inventory - #95
matthewmoorcroft wants to merge 65 commits into
Conversation
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>
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>
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>
…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>
…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>
…, fix enrich wording
…/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>
…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>
…ovable stores (cherry picked from commit 9ef2ab0)
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
Co-authored-by: Isaac <no-reply@databricks.com>
ghanse
left a comment
There was a problem hiding this comment.
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.
| 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))} |
There was a problem hiding this comment.
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.
| @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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Changed in 8951cc3: enrich checks its lock token and the inventory.json bytes it read before writing and again right before the replaces, and writes nothing if a discover ran, reporting "discover ran during this enrich; prepare and enrich again". 117260a, 0fc77bf and 6ed0b4b document the narrow windows it does not close.
…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>
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>
|
Thanks Greg, both round-3 comments are addressed.
|
Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
…agentic-insights Co-authored-by: Isaac <no-reply@databricks.com>
If os.write raised while writing the token into a new metadata/.enrich.lock (for example a full disk), the empty lock stayed behind although nothing had been written, so every later enrich refused until the lock was removed. The lock is now removed on any failure to write its token, as it already was on a short write. Co-authored-by: Isaac <no-reply@databricks.com>
The cleanup added in 872e87c unlinked the lock path unconditionally, so if a discover cleared metadata/ and a later enrich took the lock while this one's token write was failing, it removed that later enrich's lock. It now compares file identity while the descriptor is still open, and after a failed close it removes the lock only if it holds this enrich's token. Found by a Codex cross-review of 872e87c. Co-authored-by: Isaac <no-reply@databricks.com>
A second Codex cross-review found the failed-close branch had no test that a later enrich's lock survives, and that the lock docstring claimed it is left behind only when the two files may disagree, although an empty lock also stays when a failed token write cannot confirm ownership. Co-authored-by: Isaac <no-reply@databricks.com>
…entic-component Co-authored-by: Isaac <no-reply@databricks.com>
Brings #96's three fixes (authored component tasks declare and check the variables and files they name, a reloaded component field of the wrong shape is refused, parameter approximations reach SETUP.md), #95's enrich lock fix and #94's round-four fix up into #97, on top of #97's own fixes. Co-authored-by: Isaac <no-reply@databricks.com>
The merge brought up #95's enrich lock fix: when writing the token fails, enrich removes its lock only while the path is still the file it opened, so a lock a later enrich created after a discover survives. The route lock copied the earlier pattern and removed the file blindly after a short write, so it could delete a later route, fill or merge's lock and let a third command in, and an exception from the write left an empty lock behind. The route lock now follows the same rule, reusing enrich's file-identity and token checks. Tests cover a failed write removing only its own lock, and a failed write or close keeping a later command's lock. Found by the Codex cross-review of the merge. Co-authored-by: Isaac <no-reply@databricks.com>
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 rebuildsinventory.jsonby itself, fromsource_graphs.jsonplus that document under one addedinsightskey, 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.controlrelationship 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.controlrelationship must match a real(from, to, via_task_key)lineage edge markedresolved: true. Aninferredrelationship must carry evidence and a confidence level.release_state.authored_against(thesource_graphs_sha256the 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.source_graphs_sha256, the savedsource_graphs.jsonmust still be a usable object matching that hash, its source must equal the inventory'ssource, andsource_dirmust be a string. Withsource_graphs.jsonsaved, an inventory missing its hash is refused.agentic_insights.jsonis stamped with the schema version and with inventory, source-graph and insights (agentic_insights_sha256) hashes.authored_againstis checked but not stored.source_graphs.jsonwith the sharedbuild_source_inventory(project_inventory). Onlysource_dirand the hash come frominventory.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.discovery_serde.canonical_sha256is now public so the two modules share one hash.metadata/.enrich.lockfrom reading the inventory until both files are replaced, and a second call made meanwhile gets a violation and writes nothing.metadata/, the lock with it. So before writing its temp files and again right before the replaces, enrich checks that its own lock and theinventory.jsonbytes it read are unchanged; if discover ran, it writes nothing and reports "discover ran during this enrich; prepare and enrich again".agentic_insights.jsonone write ahead ofinventory.json. The lock then stays behind, so the next enrich refuses. Locally, deletemetadata/.enrich.lockand run enrich again; on the hosted MCP server, re-run discover (which clearsmetadata/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 staleinventory.jsonwhile enrich reportsok: 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.enrichsubcommand (--output-dir,--insights-path,--out) and the MCPenrichcommand. On MCP,action: "prepare"returnsmetadata/inventory.jsonfor a hosted agent that can't read the server'soutput_dir. It goes through the same size-bounded inline or volume/workspace deliverypackageuses for bundles. An inventory too large to return inline makespreparefail withok: false, naming the upload options. The defaultaction: "apply"records insights given inline or as a path.flowx-enrichskill (SKILL.md, plusinsights.mdfor the schema). It is registered inplugin.json, the README and the install docs.flowx-discovercontinues into it unless a deterministic-only run was requested; the one-shot MCPmigratecommand runs without it.7002d89(its round-four fix for Greg's comment), which includes refactor: add source-neutral discovery graph contract and shared lineage core #84's final head, is merged in, most recently asa4135ea. The earlier carries are in the branch history, anduv.lockis unchanged.How it was tested
mcpextra, which CI does not install), ruff and mypy pass.337d2d6, and Airflow output is unchanged. On those fixtures, and on an export with an empty pipeline, the inventory rebuilt fromsource_graphs.jsonis byte-identical to discover's, and enrich adds only theinsightskey.authored_against), and a failed write could leave the two files disagreeing (now a paired write with rollback).278b57e, with8fa9bacfrom the no-mistakes review:source_graphs.jsonthat 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.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;fb9b933rolls back only onOSErrorand keeps the lock for an interrupt between the two replaces.5d86088), and a hosted MCP agent had no way to read the inventory it authors against (ffe7229, theprepareaction, narrowed toinventory.jsonin7494460so large ARM files can't crowd it out of the size bound).2fb4355: a whitespace-onlyedge_identitypassed validation; it is now stripped before the non-empty check.3f4c456: an inventory with its hash deleted was trusted, the unhashedsourceandsource_dirdrove 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-shotmigrateskips 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 ina354544.preparereported 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 in117260a,0fc77bfand6ed0b4b). A process-id check in discover was tried first and replaced, because it could not tell a stale lock from a live one.7002d89carry found that an emptymetadata/.enrich.lockstayed behind when writing its token failed, so every later enrich refused. Fixed in872e87c. Two more Codex rounds with Greg's checklist then limited that cleanup to this enrich's own lock (730305c), and pinned the failed-close path and the lock docstring (960849e).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.9a0ae6b: the rebuild fromsource_graphs.json, optionalauthored_against, and the rename fromsource_insights.jsontoagentic_insights.json. Each has a test that fails without it.Merge order
Merge #84 first, then #94 retargeted to
main, then this PR (retargeted tomainonce #94 has merged), then #96 and #97.The enrich and discover docs point to the
flowx-routeskill, which lands with #97.This pull request and its description were written by Isaac.