Skip to content

feat: add per-component routing and in-engine agentic conversion - #97

Open
matthewmoorcroft wants to merge 138 commits into
stack/agentic-componentfrom
stack/routing-agentic
Open

matthewmoorcroft wants to merge 138 commits into
stack/agentic-componentfrom
stack/routing-agentic

Conversation

@matthewmoorcroft

@matthewmoorcroft matthewmoorcroft commented Sep 16, 2026 •

Copy link
Copy Markdown
Member

Intent

Route each connected component of an ADF factory to a deterministic (1:1 engine) or an agentic (agent-authored re-architecture) conversion. The user decides, and the record of the decision is one readable file. Convert stays deterministic and untouched.

  • Route writes its recommendation straight into metadata/conversion_plan.json, with every decision pending, and draws it as a standard review page.
  • The user decides by editing the plan (or passing it), and route applies it on top of convert's unchanged baseline.
  • The agent fills each routed-agentic unit with fill-agentic.
  • Package ships an agentic decision only as the agent's output, never as the deterministic stand-in, and records what was converted in metadata/route_audit.json.

The plan can be edited and re-applied as often as needed. The same plan gives the same bytes, and only what changed is re-derived. Agentic routing is ADF-only.

This builds in the conversion-approach decisions of 8 Oct:

  • Q1: package refuses until every agentic unit is filled.
  • Q2: enrich-suggested groupings that the plan records once accepted, shown on a standardized HTML review page.
  • Q3: convert's baseline is never rewritten; fills are stored separately.
  • Q4: the agent's conversion output is its own file, metadata/agentic_conversion.json, renamed from agentic_combines.json.

It also builds in:

What changed

  • Files feat: add per-component routing and in-engine agentic conversion #97 reads and writes:

    Stage Files
    describe source_graphs.json, agentic_insights.json (optional), inventory.json
    decide conversion_plan.json, routing_review.html
    convert .work/translation_report.json, .work/gaps.json, agentic_conversion.json, .work/route_baseline/ (internal)
    after the bundle, route_audit.json

    There is no separate recommendation or plan file.

  • models/conversion_plan.py, the typed ConversionPlan (schema 3):

  • routing.py:

    • Components and groupings:
      • Components are connected over proven control calls and are never split.
      • Groupings are suggested from the insights, joining components linked by an inferred relationship or by a simplification pattern recommended in more than one component.
      • A grouping's id is derived from its members, so it is stable across a re-enrich.
      • An accepted grouping becomes one agentic unit.
      • When an accepted grouping is no longer suggested, its components go back to pending and route reports it under dropped_groupings.
    • Recording the plan:
      • With no plan passed, route keeps the decisions in the recorded file (validated as written, so typos are refused).
      • It recomputes library-owned fields.
      • It applies the plan only once nothing is pending.
    • ADF-only: an inventory that records no source is not agentic-routable.
    • Reader-side insights check: insights_file_violations refuses when agentic_insights.json and the inventory's insights block disagree, or the file fails its own hash. The message says how to recover.
  • route_agentic.py:

    • Rebuild: the pure rebuild runs baseline, then stored gap fills, then the plan's units, then the agent's outputs. Applied units drop their routed gaps, including gaps nested in If, ForEach, Switch or Until.
    • Gap ownership (13 Oct): a filled unit also owns convert's gaps for activities a motif collapsed (matched_activity_names) and for activities the inventory lists for its pipelines but the report no longer has as tasks. A name a pipeline that stays deterministic also holds is kept.
    • fill-agentic:
      • Replaces fill-agentic combine and stores outputs in agentic_conversion.json.
      • Keeps a replaced history, where the first fill is not a replacement.
      • Refuses authored pipelines that still hold a placeholder, or whose names share a bundle folder once normalised.
    • Route refuses:
      • a pending decision;
      • a baseline converted from a different discover (empty ADF pipelines allowed);
      • a missing report ("run convert first"). Route no longer triggers convert, has no interactive prompt, and does not read the plan from stdin.
  • ir_serde.merge_agentic_results: after routing, an in-place merge of convert's own gaps is stored as a gap fill, and the baseline is never rewritten. The routing record's gap_fills_sha256 lets package spot a merge made after modify.

  • bundler/dab_writer.py:

    • Preflight refusals:
      • insights files that disagree (plan or no plan);
      • a routed report with no plan;
      • a plan that is not a complete, valid decision;
      • a pending decision;
      • an unfilled agentic unit;
      • an edited agent output or gap fill;
      • any report that differs from a fresh rebuild.
    • route_audit.json: carries one canonical plan hash, per-unit outcomes and replaced history, the gap fills and the routing conversation.
  • reporting/routing_review.py + routing_review_template.html: one eight-section page in the Databricks style, written by route and fill-agentic:

    1. at a glance
    2. what the source does
    3. the recommended conversion
    4. suggested groupings
    5. what happens next
    6. what you told us
    7. notes
    8. how to change a decision

    It uses the stdlib only, escapes all text, carries no timestamps, and has no logo. It is the only HTML page flowx generates.

  • adapter, mcp/server.py: route and fill_agentic follow the flow above. MCP route returns the plan and the review page because a hosted agent can't read output_dir.

  • Hosted MCP flow (13 Oct):

    • The flowx-migrate skill told Genie Code to use one MCP migrate call, which runs only discover, convert and package, so hosted runs never enriched or routed.
    • Its MCP path now runs the same steps as the CLI path, one flowx call per phase. It passes the same source to discover and convert, because convert reads the source again.
    • migrate is described as the deterministic-only shortcut in the skill, the server instructions, the tool description, app/README.md and the guide. Its behaviour is unchanged.
    • Enrich runs for every source; route and fill stay ADF-only.
  • Inputs and SQL Server ingestion (14 Oct):

    • A: migrate asks each input at the step that needs it. The start asks only for the source, the output dir and full or deterministic-only; a deterministic-only pass skips enrich, route and fill. Convert options come at convert, and catalog and schema, the bundle name and other package options at package. The hosted MCP path says the same.
    • B: catalog and schema are one free-text answer, catalog.schema (default main.default), never a "Custom" choice that can't take text. The inputs package descriptions carry that format.
    • C: SQL Server ingestion is now qualified by the routine below (N1, N2), which replaces route's three fixed SQL Server questions.
    • D: the MCP instructions and the migrate skill say that on non-ADF sources route records deterministic decisions only, and that agentic decisions and fill-agentic are ADF-only.
    • Package: it now clears resources/*.yml before writing, so a unit switched back to deterministic no longer leaves its agent-built pipeline behind to deploy. Hand-added files in resources/ are cleared too.
  • Questions at scale (14 Oct):

    • N1: enrich summarises what the analysis found, then asks at most three open questions about the facts that would rule a target pattern in or out (scale, repeat, fan-in, change, history, bar, policy, shaping), and records the answers in conversion_notes, with the source system and connector a pipeline reads from. The ADF and Airflow source guides say where each fact shows.
    • N2: route qualifies four target patterns (deterministic 1:1, Lakeflow Connect, a metadata-driven worklist, agentic re-architecture) with at most three follow-up questions, checks C1 to C13 (counting each gateway in gateway mode) and a suggestion order that matches them, and shows the failing check and its source beside a pattern it rules out. Answers stay free text in the plan's conversation, with no schema change.
    • N3: flowx keeps no copy of the platform limits. Enrich checks every pattern it recommends against the current Databricks platform limits, read live from the docs, and records each limit it relied on with its page and date in conversion_notes. Route reads the limits its checks rest on the same way, names each in words, and marks three claims the public docs don't state as assumptions: one pipeline per destination table, integrated CDC's release state, and whether a running gateway holds a concurrent-update slot. This replaced a stored platform-limits.json on 28 Oct.
    • N9: route and fill-agentic after a package now say that package pruned .work/ and to re-run convert, then route. The route, package and migrate skills describe how to change the conversion after a package, and what survives each step.
  • Answers on the open calls (14 Oct):

    • Re-running migrate: discover, and so the hosted one-shot migrate, now moves every file it would remove (plan, review page, insights, route baseline, fills, bundle) into metadata/archive/<UTC time>/ with an archive.json of what moved, from where, when and each file's hash, and says so in its result. The hosted delivery puts the current bundle before the archive.
    • Grouping accepted: a value that is not a JSON boolean ("true", 1, null) is refused when the plan loads, and package refuses such a plan.
    • Review page redesign: the approved Databricks-style page is committed without the header logo. Its deterministic counts read "recommended deterministic", since a non-ADF component can be recommended deterministic with gaps left to fill.
    • Same style for every page: the routing review page is the only HTML page flowx generates, so nothing else changed.
    • fill-agentic names: an authored pipeline whose name is not a non-empty string is refused with "give every pipeline a name" instead of crashing.
    • Old bundle folders: the post-package loop text says a re-package after the pipeline count or names change can leave the earlier bundle folders, and to remove them.
    • Fills package can build, and insights for this inventory: fill-agentic refuses an authored pipeline whose tasks is not a non-empty list of task objects, or that is marked migration_status: "excluded"; the insights check also refuses agentic_insights.json whose recorded inventory_sha256 no longer matches the inventory, in route and package.
  • sources/adf/translate.py:

    • a refused merge exits 1 with its message;
    • when convert finds no gaps it removes a stale .work/gaps.json, so the baseline is always convert's own output. Nothing in translation changes.
  • Docs: flowx-route and flowx-migrate skills, convert notes, README and guide file trees, and the AGENTS.md routing rows. Docs about refactor: add source-neutral discovery graph contract and shared lineage core #84/feat(adf): save source graphs with dataset lineage at ADF discover #94/feat: add enrich step that records agent-authored insights on the discover inventory #95 internals were left to those PRs, and the Airflow gap-resolver skill registration to Peter.

  • Version 0.2.0, with a CHANGELOG entry.

  • Fixes from checking this PR against the points raised in review (26 Oct), each with a test that fails without it and a Codex cross-review:

    • MCP route returns ok: false, uploading nothing, when the plan or the review page is missing, unreadable or left out of the size-bounded inline bundle;
    • with a plan recorded, package refuses a report with no routing record while agentic_conversion.json stores a gap fill, as after a re-convert;
    • route, fill-agentic and any merge written to the live report hold metadata/.route.lock, built like enrich's lock, and refuse while another holds it;
    • agentic_conversion.json must hold exactly components and gap_fills;
    • route reads the inventory through enrich's guard and refuses one not shaped the way discover writes it.

    Follow-ups, not in this PR: MCP package and migrate also return ok with a truncated inline bundle, whose cap counts characters (main's code); discover, convert and package take no route lock, and route, fill and package don't hold enrich's; a configured or --report copy is matched by its routing record only; duplicate JSON names keep the last value; the routing record's own key set; the plan's authored defaults; route recording the plan before an apply that fails; route_audit.json rewritten on each package; duplicate inventory pipeline names; a fill stored before this carry isn't re-checked on replay; a component fill refused by the bundle writer reports an error rather than violations.

  • Carries feat: add AgenticComponentActivity for agent-authored bundle components #96's head cb7f0d2 (with feat: add enrich step that records agent-authored insights on the discover inventory #95's lock fix and feat(adf): save source graphs with dataset lineage at ADF discover #94's round-four fix) through a no-conflict merge, 5edec1c. uv.lock is identical to feat: add AgenticComponentActivity for agent-authored bundle components #96's.

How it was tested

  • Unit tests: about 4,700 lines across 13 routing, plan, review-page, insights-check, MCP and package-preflight test files, with a regression test for each change. The full suite (1816 passed), ruff and mypy pass, and CI is green.

  • Gap ownership (13 Oct): three regression tests in test_route_agentic_flow.py, each red before the fix:

    • a collapsed motif plus an inventory-only activity, which leaves no gaps once filled;
    • two filled units sharing an inventory-only name;
    • the deterministic-pipeline guard.

    The no-mistakes test step also drove the real CLI on a hand-written ADF factory, 5 of 5 scenarios. They include package refusing the unfilled unit and switching back to deterministic, which restores gaps.json byte for byte. Deterministic-only output is still byte-identical to feat: add AgenticComponentActivity for agent-authored bundle components #96's head 07eae42.

  • Questions at scale (14 Oct): a test that route and fill-agentic after a package give the pruned-.work/ message, red before the change. A test holds the skills to reading the limits live: no copy of them in the repository, enrich's rule and record form, route's live reading, each check's limits named in words, and the three assumptions marked (red before the 28 Oct change). The no-mistakes review then added the enrich database-source flag, aligned the suggestion order with the checks, counted gateways in gateway mode, and reverted a fix round's rewrite of uv.lock to an internal package mirror.

  • Answers on the open calls (14 Oct): discover over a routed, filled and packaged folder archives every file it replaces with its bytes intact, and a fresh folder archives nothing (red on the old code); hosted migrate reports the archive and delivers the current bundle first; non-boolean accepted is refused at load and by package; a list or object pipeline name is refused by fill-agentic (each red before its fix). A fill whose tasks is a string, an object, null or a list of strings, or that is marked excluded, is refused and stores nothing, and a hand-edited deterministic inventory field is refused by route and package (red before; a missing or empty tasks list was already refused as an empty job).

  • Hosted MCP flow (13 Oct): the server's commands were driven on the ADF fixture factory as Genie would, with the definitions inline: discover, enrich (prepare and apply), convert with the same definitions, route pending, route decided, then package, which wrote route_audit.json. Convert without the source fails with --source-dir is required, which is what the skill's earlier advice missed.

  • Deterministic output: ADF and Airflow fixtures run through discover, convert and package give output trees (bundle, metadata, .work) byte-identical to feat: add AgenticComponentActivity for agent-authored bundle components #96's head 07eae42 (366 ADF files, 130 Airflow files).

  • End to end, on the full ADF fixture factory:

    1. Discover, enrich with a shared Auto Loader pattern, convert.
    2. Route with no plan gives 35 pending components and the review page.
    3. Decide, and accept the suggested grouping, then route.
    4. Package refuses the unfilled grouping.
    5. Fill it with fill-agentic.
    6. Merge one of convert's gaps; the baseline is unchanged.
    7. A re-route is a no-op.
    8. Package passes, and the audit shows the grouping agentic-applied, the gap fill and the conversation.
  • Codex cross-review, read-only:

    • Round 1: its feat: add per-component routing and in-engine agentic conversion #97 items are all fixed in the build 9221edc, each with a test:
      • a routed report with no plan;
      • names colliding after normalisation;
      • a strict plan load with package re-validation;
      • a baseline from another discover;
      • the first fill logged as a replacement, and history lost on switch-back.
    • Round 2 (on 11785e9): two P2 findings, both fixed red-green:
      • nested convert gaps surviving routing, in 517af7c;
      • malformed nested plan fields crashing instead of failing closed, in 9b8b9ae.
  • No-mistakes review rounds: then fixed, among others:

    • empty ADF pipelines blocking agentic routing;
    • a merge after modify going unnoticed;
    • grouping identity across re-enrich (member-derived ids);
    • dropped groupings reset to pending and reported;
    • hand-edited plans validated as written;
    • stale gaps.json after a re-convert;
    • Until children.
  • Deferred follow-ups:

Merge order

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

This pull request and its description were written by Isaac.

matthewmoorcroft and others added 3 commits September 16, 2026 17:26
Group pipelines into connected components over control lineage and recommend
a deterministic/agentic strategy per component (routing.py), recording the
user's decision as an additive conversion plan (models/conversion_plan.py).
Drive the in-engine agentic conversion flow (route_agentic.py) that routes,
alters and fills the remaining gaps, with plan-bound gap combine/dedupe scoped
to the routed pipeline's own tasks and mandatory bundle validation. Expose
route and fill through the adapter CLI and MCP, and update AGENTS.md.

Consolidates PRs #77 (per-connected-component routing agent), #92 and #93
(in-engine agentic conversion flow + CLI/MCP parity + cross-review fixes).

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>
…ills

Adds the first-class flowx-route skill (interactive route → decide →
record plan → fill routed-agentic groups) and updates flowx-migrate to the
discover → enrich → route → convert/fill → package flow. Registers
flowx-route and the already-present flowx-resolve-airflow-gaps in the
manifest, so it now lists both flowx-enrich (from the agentic-insights PR)
and flowx-route.

Convert runs as the deterministic baseline before routing; no plain
convert re-run after routing (only the additive merge/combine fills), so
routed-agentic placeholders survive to package. Customer decides the route
per component; the agent presents options and authors the approved fill.
Skill/manifest docs only; no src/ changes.

Co-authored-by: Isaac <no-reply@databricks.com>
matthewmoorcroft and others added 11 commits September 17, 2026 15:35
…rder

1. Airflow per-gap fill: convert --merge-agentic is ADF-only. Airflow's
   --merge-agentic is disabled and exits 2, so direct Airflow gaps to the
   flowx-resolve-airflow-gaps skill; removed the false --source airflow
   suggestion (route + migrate).
2. Merge behavior: describe it accurately — merge_agentic_results replaces
   the FIRST task whose name matches activity_name (matched by name, not
   verified to be a PlaceholderActivity). Removed the unfounded
   "replaces only placeholder tasks" guarantee (route + migrate).
3. Migrate sequencing: make the whole skill consistently linear —
   discover -> enrich -> convert (deterministic baseline) -> route
   (decide + edit) -> fill agentic gaps -> package. Fixed frontmatter,
   intro, and phase list; reordered and renumbered the workflow steps
   (convert now Step 5 before route Step 7, fill Step 8) so it reads
   top-to-bottom in execution order with no forward/back jumps; reordered
   the MCP command sequence to put convert before route.

Grounded against adapter/__main__.py, sources/airflow/convert.py, and
ir_serde.py. Skill-doc edits only; no src/ changes. make fmt + make test
green.

Co-authored-by: Isaac <no-reply@databricks.com>
merge_agentic_results / _find_and_replace_task (ir_serde.py) replaces the
matched task with the authored task definition verbatim and does not set or
mutate any `status` field. Reword the flowx-route merge note to describe the
in-place replacement accurately and drop the false "status becomes translated"
claim, consistent with the name-match description above it.

Skill-doc edit only; no src/ changes. make fmt + make test green.

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

Airflow discovery does not yet emit control lineage or motifs
(sources/airflow/discover.py), so it cannot form routing components, and
convert --merge-agentic --source airflow is disabled
(sources/airflow/convert.py). The route -> edit -> fill agentic-conversion
flow is therefore ADF-only today; Airflow aligns via #63.

- flowx-route: removed the false "Airflow (RunJob) emit control edges,
  source-neutral routing" claim; added an "ADF only (current scope)" note
  near the top directing Airflow agentic gaps to flowx-resolve-airflow-gaps.
- flowx-migrate: added the ADF-only scope caveat to the Context intro and
  Step 7 (route); deterministic discover/convert/package still described as
  working for both sources.

Skill-doc edits only; no src/ changes. make fmt + make test green.

Co-authored-by: Isaac <no-reply@databricks.com>
…A-grounded channel

FIX 2: apply_combine_fill now asserts every agent-authored pipeline carries
tags.source == 'adf' (routing/agentic is ADF-only). A missing/wrong tag fails
the combine closed (nothing written) with a clear message, instead of being
accepted at combine and only rejected later at package preflight. The required
tag is now documented in the flowx-route AgenticComponentActivity / combine
authoring example.

FIX 3: Make combine idempotent. The recorded plan still lists a component's
members after a prior combine collapsed them out of the report, so the
plan/membership check passes on a re-run; a naive second run then re-appends the
authored pipeline (duplicate). Detect the already-combined state (members gone
from the report, authored pipeline(s) already present) and no-op
(already_combined=true). Regression test: running combine twice with the same
members + authored pipelines yields a single authored pipeline.

FIX 5: Persist a compact metadata/route_audit.json (routed components +
decisions + gaps introduced) before package prunes .work/, so the "what did
routing change?" trail survives the default prune. Additive and non-breaking: it
writes nothing when no conversion_plan.json was recorded, so the no-route path
stays byte-identical. .work/ pruning is unchanged.

FIX 8b: In flowx-route's fill authoring guidance, default the Lakeflow
pipeline/Connect resource channel to 'current' (stable/GA); only use 'preview'
with a cited GA-status justification + confirmed workspace availability. Require
the same GA-vs-Preview verification/citation as flowx-enrich (FIX 8a) before
recommending any connector / Lakeflow Connect pattern.

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

Bring the discover warning/units, enrich edge_type guidance, and GA-grounding
into the tip so it carries all Phase-1 fixes (1, 7, 4, 8a, 2, 3, 5, 8b).

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

Cross-review BLOCKING 1: the prior FIX 3 detection inferred "already combined"
from pipeline-name sets, which broke two ways:
  (a) re-running combine with the SAME --members but a differently-named authored
      replacement fell through to combine_group_fill (members already gone, so
      nothing dropped) and appended a second authored pipeline -> duplicate;
  (b) an authored pipeline whose name collided with a former member defeated the
      detection entirely.

Fix: each successful combine now stamps an additive `_combine_provenance` marker
onto the report recording (component_id, inventory_sha256). Idempotency keys off
that recorded state -- never off comparing authored names to member names -- so a
re-run is detected regardless of how the authored replacement is named or whether
a name collides. The marker lives in the report, so a fresh convert (which
rewrites the report) clears it and combine re-applies after a genuine re-convert.
The marker is an extra top-level report key: ignored by the package phase (not a
recognized report shape key) and by ir_serde (which reads per-pipeline IR, not the
report wrapper), so the convert->package contract shape is unchanged.

Regression tests cover both failure modes: (a) a differently-named authored re-run
does not duplicate, and (b) an authored name colliding with a former member is
still detected as already-combined.

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

Bring the --adf-source-path warning correction and the enrich GA-grounding rework
into the tip, so it carries all cross-review re-fixes (FIX 1, 8a) alongside its
own FIX 3 idempotency rework.

Co-authored-by: Isaac <no-reply@databricks.com>
…nto stack/routing-agentic

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

Mirror the per-pattern release_state / release_state_source onto AgenticPattern
(they already ride into the agentic option via the {**pattern} spread; this makes
the parity explicit and typed). Add a neutral, per-pattern release_disclosures list
to AgenticOption: one entry per recommended pattern whose state is disclosed
(public_preview / private_preview / beta), carrying the pipeline, pattern, state, a
factual label, and a human-readable message. It is pure factual labelling, not a
warning:

- ga / unknown -> silent (no entry; unknown is treated exactly like ga);
- public_preview -> label "Public Preview (production-ready)";
- private_preview -> label "Private Preview";
- beta -> label "Beta".

The route skill points its channel guidance at the structured release_state and
reflects the neutral labels. The separate recommendation-eligibility rule (treat
Private Preview as doNotSuggest without confirmed enrollment) and the
verify-before-recommend grounding rule are unchanged.

Co-authored-by: Isaac <no-reply@databricks.com>
… stack/routing-agentic

Co-authored-by: Isaac <no-reply@databricks.com>
…ack/routing-agentic

Co-authored-by: Isaac <no-reply@databricks.com>
matthewmoorcroft and others added 10 commits September 18, 2026 17:07
…lease_state examples) into stack/routing-agentic

Co-authored-by: Isaac <no-reply@databricks.com>
The Module Descriptions table and the "Adding a New Deterministic
Translator" guide still pointed at modules removed in the source-split
refactor (parser/adf_loader.py, translator/engine.py,
translator/activity_translators/). Repoint them at the current
sources/<source>/ layout (sources/adf/loader.py,
sources/adf/translate.py, sources/adf/translators/) and add the new
modules this branch introduces (sources/adf/discovery_mapping.py,
sources/airflow/, route_agentic.py, models/discovery.py).

Co-authored-by: Isaac <no-reply@databricks.com>
CLAUDE.md previously re-imported AGENTS.md via the `@AGENTS.md` line.
Replace it with a real relative symlink (git mode 120000) so AGENTS.md
is the single canonical onboarding doc and the two never drift.

Co-authored-by: Isaac <no-reply@databricks.com>
Record the bar for maintaining AGENTS.md now that it is the single
canonical onboarding doc, so future edits keep it concise and pointer-first.

Co-authored-by: Isaac <no-reply@databricks.com>
Agentic routing is ADF-only in Phase 1, but only fill-agentic combine checked
the source: route with an agentic decision on an Airflow inventory recorded
the plan and rewrote the DAG's deterministic tasks into placeholder gaps,
outside the Airflow per-gap resolver. validate_plan now rejects an agentic
decision unless the inventory source is adf (an inventory with no source
predates the unified emitter and counts as ADF), and the recommendation for
such an inventory is deterministic with a finding that says why. The route
and migrate skills say this instead of promising Airflow routing once #63
lands. Route test fixtures now declare source adf.

Co-authored-by: Isaac <no-reply@databricks.com>
Package now refuses to write a bundle when metadata/conversion_plan.json was
recorded against a different inventory fingerprint than the current one, or
when the report's combine provenance does not match an agentic component in
the plan. route records the hash of the report it first edited under
_route_provenance, and route_audit.json records the current inventory
fingerprint plus the plan, baseline report and packaged report hashes.

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

Co-authored-by: Isaac <no-reply@databricks.com>
… graphs, flowx-owned passthrough fields) into stack/routing-agentic

Co-authored-by: Isaac <no-reply@databricks.com>
The shared emitter now reads motifs from each graph's lineage, so the
routing recommendation tests attach them with the ADF loader's helper.

Co-authored-by: Isaac <no-reply@databricks.com>
conversion_plan.json was built and read as raw dicts; the dataclasses only
backed tests. ConversionPlan now loads, round-trips and writes the plan and
every reader goes through it: route records it, apply_plan applies it to
the IR after convert, the combine fill and package check it, and the route
audit reads it.

Schema 2 binds the plan to what it was decided on: the inventory
fingerprint, the saved source_graphs.json hash the inventory records, and
the saved source_insights.json hash when enrich ran. Package refuses to run
when any of them no longer match. Decisions stay per component; each
component carries a reserved assignments list for per-node or subgraph
routing (deterministic, agentic with a named pattern, or mixed at a
boundary), which must stay empty for now.

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

Copy link
Copy Markdown
Member Author

Thanks Greg. All ten are fixed, each with a test that failed first:

  1. Gaps on rebuild: route's rebuild now drops convert's gap for each applied gap fill and for anything nested in the filled task. Convert's gaps carry no pipeline, so a (pipeline, activity_name) filter would never match them; the new drop_filled_gaps goes by name and keeps a gap only when a pipeline outside the routed units still has that name open. The tests now start from a real gaps.json. b1db89f, 40eba35
  2. Gaps on the live merge: every in-place merge into .work/translation_report.json, routed or not, takes the same gaps out of .work/gaps.json with the same function, so the merge and a later route write the same gaps. A merge to an --output copy leaves gaps.json alone, and an unreadable gaps.json no longer stops a merge half way. b1db89f, 9d8ef25, 40eba35
  3. taken_names: only the outputs the rebuild applies reserve names now, read from one helper the rebuild and the fill share, so switching a unit back frees its names. 867deb3
  4. Carry-forward ids: each carried row takes its component's current id, so a decision follows its pipelines when the ids shift. ff554d9
  5. Missing edge source: both ends are checked; an edge whose source is empty, not a string or not an inventory pipeline joins nothing and is a finding. ab84fc3
  6. Private Preview: the route skill now says what enrich and the code do: the state is disclosed, with no doNotSuggest gate. d256374
  7. NodeAssignment: removed with ASSIGNMENT_ROUTES and the loading and checking code; an assignments key is now refused like any other unknown key. ea070d9
  8. agentic_pipeline_names: built from routing_units, so there is one answer. 4b387fa
  9. Decided but not applied: a unit decided since the last route shows "Decided, not applied yet" with "Run route again", checked against the routing record with the same per-unit rule package uses. An accepted grouping with a component still pending shows as pending, since route applies nothing until it is decided. 3bb0475, 40eba35, bf07b01
  10. ForEach and Switch: added to every nested-gap test, including the new gap-fill one. f4ad628

The branch also merges #96's round-three head (3406913, no conflicts). At bf07b01 the unit suite passes (2454) and ruff and mypy are clean.

matthewmoorcroft added a commit that referenced this pull request Oct 21, 2026
Conflicts in bundler/dab_writer.py, resolved keeping both sides:
- write_bundle_group keeps main's per-workflow loop and drops the old
  resources/*.yml clearing refresh 5 placed there; #97 now archives the
  resource files a package did not write, after writing.
- main()'s packaging modes keep their own preflights; once a mode's
  preflight passes, #97's _archive_stale_bundles runs with that mode's
  real bundle folders (the output folder for single or shared Airflow,
  else one folder per group), before anything is written.
- #97's option A code uses main's to_lowercase_key, and the top-level
  bundle's entries include main's pyproject.toml.

Integration adaptations: discover's reset also moves main's DEPLOY.md and
pyproject.toml (the test's managed list follows), and the package skill's
option A text names packaging modes.

Co-authored-by: Isaac <no-reply@databricks.com>
Brings #94's fix (c38da87) up through #95 (0e7ad99) and #96: discover drops
the fields an ADF REST GET adds without meaning before it keeps the source,
so a live export and a Git or ARM export give the same graphs and hash.

Co-authored-by: Isaac <no-reply@databricks.com>
matthewmoorcroft added a commit that referenced this pull request Oct 21, 2026
Co-authored-by: Isaac <no-reply@databricks.com>
matthewmoorcroft and others added 26 commits October 26, 2026 16:13
…ndle

MCP route returns the recorded conversion_plan.json and routing_review.html
through the same size-bounded inline bundle as enrich prepare. A file over
the 2 MB cap was listed under truncated and left out, but route still
returned ok: true, so a hosted agent could be told to edit a plan it never
received. Route now returns ok: false with an error naming each file left
out and its size, and the upload paths to pass instead, the same rule enrich
prepare follows for the inventory. Uploads are unchanged. The tool
description and the route skill say so. Tests cover each file over the cap
and an upload of an oversized plan.

Co-authored-by: Isaac <no-reply@databricks.com>
Route stamps a routing record whenever it applies a stored gap fill, even
when every component is deterministic. Package only checked the plan when
the report had no record: an all-deterministic plan passed without reading
agentic_conversion.json. So after a re-convert, package shipped the fresh
report and silently left out the stored fill. Package now refuses a report
with no record when the store holds a gap fill, and says to re-run route,
which applies the fill or drops it when it was merged against an earlier
convert. The route skill lists the refusal. A test re-converts after a merge
on an all-deterministic plan, checks package refuses, then re-routes and
checks the package carries the fill.

Co-authored-by: Isaac <no-reply@databricks.com>
A successful route writes conversion_plan.json and routing_review.html in
the same call, but MCP route delivered only the files it found and still
returned ok: true when one was gone, for example after a discover in
between. It now returns ok: false naming the missing file and saying to run
route again. After a failed route it still returns whichever file already
exists. The test double now writes both files the way the real command
does. Found by the Codex cross-review of the inline-cap fix.

Co-authored-by: Isaac <no-reply@databricks.com>
With no conversion plan the folder is not routed, so package ignores the
stored agent outputs and gap fills; that is how a report whose plan went
missing is packaged without routing after a re-convert. The route skill's
new refusal line now says so, as the Codex cross-review pointed out.

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

MCP route checked that the plan and review page existed and then copied
them, so a discover moving one in between raised out of the command instead
of returning ok: false. It now copies each file first and counts one that
cannot be copied as missing, taking sizes from the copies. The docstring,
the tool description and the route skill now say ok is false when either
file is missing (run route again) or too large to return inline (pass an
upload path). Both found by the Codex cross-review of the previous commit.

Co-authored-by: Isaac <no-reply@databricks.com>
fill-agentic loaded agentic_conversion.json, then wrote the store, the
report and its gaps with nothing held in between, so two fills on one
output folder (an agent filling two components in one turn over MCP) both
returned ok: true and the last write dropped the other unit. Route and a
merge into the live report rewrite the same files the same way.

The three now hold metadata/.route.lock while they work, a file created
with O_EXCL holding a random token, the shape enrich's lock already uses,
and refuse with a violation, writing nothing, while another holds it. The
lock is removed when the command finishes or raises; only a killed run
leaves it, and the message says how to clear it locally and on the hosted
server. A merge into a separate copy takes no lock. The route skill and the
MCP tool description say to run them one at a time per output folder.

Tests hold the lock and check that fill, route and an in-place merge each
refuse and write nothing, that a merge into a copy still runs, and that
each command, including a fill that raises, leaves no lock behind.

Co-authored-by: Isaac <no-reply@databricks.com>
Three gaps the Codex cross-review found in route's file delivery:
- after a failed route, a file over the inline cap replaced the whole
  bundle with a size error, though a failed route should return whichever
  files exist as they are; it now does, with truncated listing the file;
- a file the inline reader skipped because it is not UTF-8 text was
  reported as too large; it is now named as not text;
- any copy error was reported as a missing file to fix by re-running
  route; only a missing file is, and other errors carry their reason.
The too-large check now reads the bundle's truncated list. Tests cover each
case.

Co-authored-by: Isaac <no-reply@databricks.com>
load_agentic_output checked only components and gap_fills, and every save
rebuilds metadata/agentic_conversion.json from those two keys, so any other
top-level key was accepted and then dropped without a word on the next
fill, merge or re-route. The loader now refuses a key outside those two,
naming it, so route, fill-agentic, the merge and package all refuse that
store and nothing is rewritten. The route skill's list of package refusals
says so. A test adds a hand-written key and a lookalike key to a filled
store and checks that the load and the next fill refuse, with the store
left as it was.

Co-authored-by: Isaac <no-reply@databricks.com>
Route iterated inventory["pipelines"] directly in four places in routing.py
and two in route_agentic.py, so a hand-edited inventory whose pipelines is
a number or null crashed route with a TypeError. Enrich already reads the
same list through _inventory_pipelines, which treats a value that is not a
list as listing no pipelines and keeps only object entries. Route now reads
it through that same helper, so both phases give one answer. Tests cover
components and the recommendation for int, dict, string and null values,
and a rebuild over such an inventory, where the baseline check still
refuses the mismatch.

Co-authored-by: Isaac <no-reply@databricks.com>
After a successful route, MCP route uploaded both files before checking
them, so with an upload path a plan that is not text went out with
ok: true, and a missing page still let the new plan be uploaded beside an
older page while the error came back. It now copies and reads each file as
UTF-8 first, and after a successful route returns the error, with nothing
uploaded, when either is missing, cannot be copied or is not text; only then
does it deliver, refusing an inline bundle that left either out for size.
A failed route still returns whichever files exist. Each cause is named
from the error actually raised. The docstring, tool description and route
skill list the unreadable case and say nothing is uploaded. Tests cover a
missing page and a non-text plan on both upload paths. Found by the Codex
cross-review of the previous commit.

Co-authored-by: Isaac <no-reply@databricks.com>
Three gaps the Codex cross-review found in the route lock:
- a merge from another file into an output folder's live
  .work/translation_report.json overwrote it without the lock, because only
  a merge in place took it; any merge written to a live report now holds
  that folder's lock;
- an output folder with no metadata/ yet held nothing, so two merges there
  could both run; the lock now creates metadata/ and holds it, and only a
  folder that does not exist is left unheld;
- the hosted recovery for a stale lock said discover, then route and fill,
  but discover also moves .work/, so convert has to run first; the message
  and the route skill now say enrich if used, convert, route and fill.
Tests cover each.

Co-authored-by: Isaac <no-reply@databricks.com>
The store loader read a missing components or gap_fills key as empty, so
deleting gap_fills by hand silently dropped every stored fill on the next
route, fill or merge. Every save writes both keys, so the loader now
requires exactly those two, naming any unknown or missing key. The test now
covers both kinds of key and checks that the load, the next fill, route and
package each refuse with the store left as it was. Found by the Codex
cross-review of the unknown-key fix.

Co-authored-by: Isaac <no-reply@databricks.com>
The docstring said a failed MCP route returns whichever of the plan and the
review page exist as they are, but the inline bundle carries only UTF-8
text. It now says so.

Co-authored-by: Isaac <no-reply@databricks.com>
Reading a malformed inventory list as empty kept route from crashing, but
the Codex cross-review showed it can also make a pipeline look fully
deterministic: activities written as an object read as none, so the
pipeline had no agentic activity left, and a motif's member_task_keys
written as a string covered every activity named by one of its letters.
Values of 1 for activities or motifs, or a non-object root, still crashed.

The route CLI now checks the inventory's shape before anything else and
refuses, writing nothing, when the root is not an object, pipelines is not
a list of objects, a pipeline's activities or motifs is not a list of
objects, or a motif's member_task_keys is not a list of names, saying to
run discover again. The readers themselves read such values as none
without crashing, and motif coverage takes only names. The route skill
says so. Tests cover each shape through the route command and the readers.

Co-authored-by: Isaac <no-reply@databricks.com>
Route iterated the insights' pipeline_insights, recommended_patterns and
pipeline_relationships without checking they were lists, so an edited
insights block whose hashes were recomputed crashed route with a
TypeError. Enrich validates these lists when it records them, and they
only feed the agentic patterns and suggested groupings, so a value that is
not a list now reads as none: it can only take a suggestion away, never add
a capability. Tests cover each list. Found by the Codex cross-review of the
inventory-list fix.

Co-authored-by: Isaac <no-reply@databricks.com>
From the Codex cross-reviews of the lock and store follow-ups:
- the stale-lock recovery said to run route again, but a merge or fill that
  was refused or stopped is not re-applied by route alone; the message and
  the route skill now say to run route and then that command again, and on
  the hosted server every fill and merge after convert and route;
- the MCP tool description said only an in-place merge holds the lock; it
  now says any merge written to the live report does;
- the copy-to-live merge test now keeps the source copy in a separate
  folder, so it fails if the lock were taken on the source's folder;
- a new test merges a real gap fill, deletes the store's gap_fills key and
  checks the next merge refuses with the store left as it was.

Co-authored-by: Isaac <no-reply@databricks.com>
The final Codex cross-review found two gaps in the inventory shape check:
- a pipeline with no activities key, or no name, passed, so route read it
  as fully deterministic with nothing to convert, or dropped it; discover
  always writes both (motifs only when it found some), so route now refuses
  a pipeline without a non-empty name or an activities list;
- the library's record_plan, which the route command calls, did not run
  the check, so another caller could record a plan over a malformed
  inventory; it now refuses the same shapes, writing nothing.
The route skill lists the new cases. Tests cover a missing activities key,
an empty name, and record_plan.

Co-authored-by: Isaac <no-reply@databricks.com>
From the Codex cross-review of the inventory name and activities check:
- a motif with no member_task_keys passed the shape check and read as
  covering nothing, though discover always writes the key; it is now
  refused like a member_task_keys that is not a list of names;
- record_plan checked the inventory before its own arguments, so a call
  with no plan over a malformed inventory returned a shape failure instead
  of the documented ValueError; it now reads the plan argument first.
Tests cover both.

Co-authored-by: Isaac <no-reply@databricks.com>
From the Codex cross-review of the previous commit:
- a motif with no string motif_id still counted as covering its members,
  and a numeric id was turned into text; discover always writes a string
  id, so the shape check now refuses any other, and the reader takes only
  string ids;
- record_plan read the inventory before the plan argument, so with no
  plan source and an inventory that is not an object it raised the
  inventory error, not the documented one; it now reads the plan argument
  before any file.
Tests cover a numeric motif id and both record_plan cases.

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>
flowx no longer keeps skills/flowx-route/platform-limits.json, a dated
copy of the Databricks limits that would need maintaining as they
change. Its shape test goes with it.

Enrich now checks every pattern it recommends against the current
Databricks platform limits, read live from the docs. It doesn't
recommend a pattern a limit rules out, records each limit it relied on
in the conversion_notes of every pipeline the recommendation covers, as
"limit: <name> = <value> (source: <page>, read <date>)", marks a pattern
"not qualified" when a fact or the docs are missing, and never uses a
remembered number. Release state keeps its own rule.

Route keeps checks C1 to C13, the arithmetic and the suggestion order,
and reads the limits the same way. Each check now names its limits in
words. The three claims the public docs don't state (one pipeline per
destination table, a running gateway holding a concurrent update, and
integrated CDC's release state) are marked as assumptions. The gateway
count G and the For each concurrency F are read live and used in the
checks instead of being fixed.

A new test holds the skill text to this: no copy of the limits anywhere
in the repository, enrich's rule and record form, route's live reading,
each check's limits in words, and the three assumptions marked. The
changelog line says the limits are read live.

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